From 0de404d3d7a49b368e8413a718ab25024def1cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= <34384633+tdelabro@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:27:53 +0200 Subject: [PATCH] feat: impl TryInto for Felt (#74) Co-authored-by: Pedro Fontana --- crates/starknet-types-core/src/felt/mod.rs | 54 +-- .../src/felt/primitive_conversions.rs | 358 ++++++++++++++++++ fuzz/felt/fuzz_targets/conversions.rs | 64 ++++ rust-toolchain.toml | 2 +- 4 files changed, 427 insertions(+), 51 deletions(-) create mode 100644 crates/starknet-types-core/src/felt/primitive_conversions.rs diff --git a/crates/starknet-types-core/src/felt/mod.rs b/crates/starknet-types-core/src/felt/mod.rs index 7974ee94..11d99b93 100644 --- a/crates/starknet-types-core/src/felt/mod.rs +++ b/crates/starknet-types-core/src/felt/mod.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod felt_arbitrary; +mod primitive_conversions; use core::ops::{Add, Mul, Neg}; use core::str::FromStr; @@ -38,13 +39,14 @@ use lambdaworks_math::{ #[cfg(feature = "arbitrary")] use arbitrary::{self, Arbitrary, Unstructured}; -#[repr(transparent)] /// Definition of the Field Element type. +#[repr(transparent)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Felt(pub(crate) FieldElement); /// A non-zero [Felt]. -#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NonZeroFelt(FieldElement); impl NonZeroFelt { @@ -533,33 +535,6 @@ impl TryFrom<&Felt> for NonZeroFelt { } } -impl From for Felt { - fn from(value: u128) -> Felt { - Self(FieldElement::from(&UnsignedInteger::from(value))) - } -} - -impl From for Felt { - fn from(value: i128) -> Felt { - let mut res = Self(FieldElement::from(&UnsignedInteger::from( - value.unsigned_abs(), - ))); - if value.is_negative() { - res = -res; - } - res - } -} - -impl From for Felt { - fn from(value: bool) -> Felt { - match value { - true => Felt::ONE, - false => Felt::ZERO, - } - } -} - impl From<&BigInt> for Felt { fn from(bigint: &BigInt) -> Felt { let (sign, bytes) = bigint.to_bytes_le(); @@ -595,27 +570,6 @@ impl From for Felt { } } -macro_rules! impl_from { - ($from:ty, $with:ty) => { - impl From<$from> for Felt { - fn from(value: $from) -> Self { - (value as $with).into() - } - } - }; -} - -impl_from!(u8, u128); -impl_from!(u16, u128); -impl_from!(u32, u128); -impl_from!(u64, u128); -impl_from!(usize, u128); -impl_from!(i8, i128); -impl_from!(i16, i128); -impl_from!(i32, i128); -impl_from!(i64, i128); -impl_from!(isize, i128); - impl FromStr for Felt { type Err = FromStrError; diff --git a/crates/starknet-types-core/src/felt/primitive_conversions.rs b/crates/starknet-types-core/src/felt/primitive_conversions.rs new file mode 100644 index 00000000..9e361674 --- /dev/null +++ b/crates/starknet-types-core/src/felt/primitive_conversions.rs @@ -0,0 +1,358 @@ +use lambdaworks_math::{field::element::FieldElement, unsigned_integer::element::UnsignedInteger}; + +use super::Felt; + +// Bool <-> Felt + +impl From for Felt { + fn from(value: bool) -> Felt { + match value { + true => Felt::ONE, + false => Felt::ZERO, + } + } +} + +// From for Felt + +macro_rules! impl_from { + ($from:ty, $with:ty) => { + impl From<$from> for Felt { + fn from(value: $from) -> Self { + (value as $with).into() + } + } + }; +} + +impl_from!(u8, u128); +impl_from!(u16, u128); +impl_from!(u32, u128); +impl_from!(u64, u128); +impl_from!(usize, u128); +impl_from!(i8, i128); +impl_from!(i16, i128); +impl_from!(i32, i128); +impl_from!(i64, i128); +impl_from!(isize, i128); + +impl From for Felt { + fn from(value: u128) -> Felt { + Self(FieldElement::from(&UnsignedInteger::from(value))) + } +} + +impl From for Felt { + fn from(value: i128) -> Felt { + let mut res = Self(FieldElement::from(&UnsignedInteger::from( + value.unsigned_abs(), + ))); + if value.is_negative() { + res = -res; + } + res + } +} + +// TryFrom for primitive + +#[derive(Debug, Copy, Clone)] +pub struct PrimitiveFromFeltError; + +#[cfg(feature = "std")] +impl std::error::Error for PrimitiveFromFeltError {} + +impl core::fmt::Display for PrimitiveFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Failed to convert `Felt` into primitive type") + } +} + +macro_rules! impl_try_felt_into_unsigned { + ($into: ty) => { + impl TryFrom for $into { + type Error = PrimitiveFromFeltError; + + fn try_from(value: Felt) -> Result<$into, Self::Error> { + let bytes_be = value.to_bytes_le(); + let (bytes_to_return, bytes_to_check) = + bytes_be.split_at(core::mem::size_of::<$into>()); + + if bytes_to_check.iter().all(|&v| v == 0) { + Ok(<$into>::from_le_bytes(bytes_to_return.try_into().unwrap())) + } else { + Err(PrimitiveFromFeltError) + } + } + } + }; +} + +impl_try_felt_into_unsigned!(u8); +impl_try_felt_into_unsigned!(u16); +impl_try_felt_into_unsigned!(u32); +impl_try_felt_into_unsigned!(u64); +impl_try_felt_into_unsigned!(usize); +impl_try_felt_into_unsigned!(u128); + +const MINUS_TWO_BYTES_REPR: [u8; 32] = [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 16, 0, 0, 0, 0, 0, 0, 8, +]; + +macro_rules! impl_try_felt_into_signed { + ($into: ty) => { + impl TryFrom for $into { + type Error = PrimitiveFromFeltError; + + // Let's call `s` the `size_of_type`. + // + // There is 3 ways the value we are looking for can be encoded in the Felt bytes: + // 1. Positive numbers: + // Bytes [s..] are all set to zero, and byte [s] value must not exceed 127 + // 2. Negative numbers: + // Bytes [s..] should match `MINUS_TWO_BYTES_REPR`, and byte [s] value must not be less than 128 + // 3. -1: + // Bytes are those of Felt::MAX + fn try_from(value: Felt) -> Result { + // We'll split the conversion in 3 case: + // Felt >= 0 + // Felt < -1 + // Felt == -1 + + // Get the size of the type we'll convert the felt into + let size_of_type = core::mem::size_of::<$into>(); + // Convert the felt as little endian bytes + let bytes_le = value.to_bytes_le(); + + // Case 1: Felt >= 0 + // Our felt type can hold values up to roughly 2**252 bits which is encoded in 32 bytes. + // The type we'll convert the value into can hold `size_of_type` (= { 1, 2, 4, 8, 16 }) bytes. + // If all the bytes after the last byte that our target type can fit are 0 it means that the + // number is positive. + // The target type is a signed type which means that the leftmost bit of the last byte + // (remember we're in little endian) is used for the sign (0 for positive numbers) + // so the last byte can hold a value up to 0b01111111 + if bytes_le[size_of_type..].iter().all(|&v| v == 0) + && bytes_le[size_of_type - 1] <= 0b01111111 + { + Ok(<$into>::from_le_bytes( + bytes_le[..size_of_type].try_into().unwrap(), + )) + } + // Case 2: Felt < -1 + // Similarly to how we checked that the number was positive by checking the bytes after the + // `size_of_type` byte we check that all the bytes after correspond to the bytes of a negative felt + // The leftmost bit is use for the sign, as it's a negative number it has to be 1. + else if bytes_le[size_of_type..] == MINUS_TWO_BYTES_REPR[size_of_type..] + && bytes_le[size_of_type - 1] >= 0b10000000 + { + // We take the `size_of_type` first bytes because they contain the useful value. + let offsetted_value = + <$into>::from_le_bytes(bytes_le[..size_of_type].try_into().unwrap()); + + // Quite conveniently the byte representation of the `size_of` least significant bytes + // is the one of the negative value we are looking for +1. + // So we just have to remove 1 to get the actual value. + offsetted_value.checked_sub(1).ok_or(PrimitiveFromFeltError) + // Case 3: Felt == -1 + // This is the little endian representation of Felt::MAX. And Felt::MAX is exactly -1 + // [ + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 8, + // ] + // The only valid Felt with those most significant bytes is Felt::MAX. + // All other arrays with those most significant bytes are invalid Felts because they would be >= Prime. + } else if bytes_le[24..] == [17, 0, 0, 0, 0, 0, 0, 8] { + return Ok(-1); + } else { + Err(PrimitiveFromFeltError) + } + } + } + }; +} + +impl_try_felt_into_signed!(i8); +impl_try_felt_into_signed!(i16); +impl_try_felt_into_signed!(i32); +impl_try_felt_into_signed!(i64); +impl_try_felt_into_signed!(isize); +impl_try_felt_into_signed!(i128); + +#[cfg(test)] +mod tests { + use crate::felt::Felt; + #[test] + fn from_and_try_from_works_both_way_for_valid_unsigned_values() { + // u8 + let u8_max_value = u8::MAX; + assert_eq!(u8_max_value, Felt::from(u8_max_value).try_into().unwrap()); + let u8_42_value = 42u8; + assert_eq!(u8_42_value, Felt::from(u8_42_value).try_into().unwrap()); + let u8_zero_value = 0u8; + assert_eq!(u8_zero_value, Felt::from(u8_zero_value).try_into().unwrap()); + // u16 + let u16_max_value = u16::MAX; + assert_eq!(u16_max_value, Felt::from(u16_max_value).try_into().unwrap()); + let u16_42_value = 42u16; + assert_eq!(u16_42_value, Felt::from(u16_42_value).try_into().unwrap()); + let u16_zero_value = 0u16; + assert_eq!( + u16_zero_value, + Felt::from(u16_zero_value).try_into().unwrap() + ); + // u32 + let u32_max_value = u32::MAX; + assert_eq!(u32_max_value, Felt::from(u32_max_value).try_into().unwrap()); + let u32_42_value = 42u32; + assert_eq!(u32_42_value, Felt::from(u32_42_value).try_into().unwrap()); + let u32_zero_value = 0u32; + assert_eq!( + u32_zero_value, + Felt::from(u32_zero_value).try_into().unwrap() + ); + // u64 + let u64_max_value = u64::MAX; + assert_eq!(u64_max_value, Felt::from(u64_max_value).try_into().unwrap()); + let u64_42_value = 42u64; + assert_eq!(u64_42_value, Felt::from(u64_42_value).try_into().unwrap()); + let u64_zero_value = 0u64; + assert_eq!( + u64_zero_value, + Felt::from(u64_zero_value).try_into().unwrap() + ); + // u128 + let u128_max_value = u128::MAX; + assert_eq!( + u128_max_value, + Felt::from(u128_max_value).try_into().unwrap() + ); + let u128_42_value = 42u128; + assert_eq!(u128_42_value, Felt::from(u128_42_value).try_into().unwrap()); + let u128_zero_value = 0u128; + assert_eq!( + u128_zero_value, + Felt::from(u128_zero_value).try_into().unwrap() + ); + // usize + let usize_max_value = usize::MAX; + assert_eq!( + usize_max_value, + Felt::from(usize_max_value).try_into().unwrap() + ); + let usize_42_value = 42usize; + assert_eq!( + usize_42_value, + Felt::from(usize_42_value).try_into().unwrap() + ); + let usize_zero_value = 0usize; + assert_eq!( + usize_zero_value, + Felt::from(usize_zero_value).try_into().unwrap() + ); + } + + #[test] + fn from_and_try_from_works_both_way_for_all_valid_i8_and_i16_values() { + // i8 + for i in i8::MIN..i8::MAX { + assert_eq!(i, Felt::from(i).try_into().unwrap()); + } + // i16 + for i in i16::MIN..i16::MAX { + assert_eq!(i, Felt::from(i).try_into().unwrap()); + } + // For the others types it would be too long to test every value exhaustively, + // so we only check keys values in `from_and_try_from_works_both_way_for_valid_signed_values` + } + + #[test] + fn from_and_try_from_works_both_way_for_valid_signed_values() { + // i32 + let i32_max = i32::MAX; + assert_eq!(i32_max, Felt::from(i32_max).try_into().unwrap()); + let i32_min = i32::MIN; + assert_eq!(i32_min, Felt::from(i32_min).try_into().unwrap()); + let i32_zero = 0i32; + assert_eq!(i32_zero, Felt::from(i32_zero).try_into().unwrap()); + let i32_minus_one = -1i32; + assert_eq!(i32_minus_one, Felt::from(i32_minus_one).try_into().unwrap()); + // i64 + let i64_max = i64::MAX; + assert_eq!(i64_max, Felt::from(i64_max).try_into().unwrap()); + let i64_min = i64::MIN; + assert_eq!(i64_min, Felt::from(i64_min).try_into().unwrap()); + let i64_zero = 0i64; + assert_eq!(i64_zero, Felt::from(i64_zero).try_into().unwrap()); + let i64_minus_one = -1i64; + assert_eq!(i64_minus_one, Felt::from(i64_minus_one).try_into().unwrap()); + // isize + let isize_max = isize::MAX; + assert_eq!(isize_max, Felt::from(isize_max).try_into().unwrap()); + let isize_min = isize::MIN; + assert_eq!(isize_min, Felt::from(isize_min).try_into().unwrap()); + let isize_zero = 0isize; + assert_eq!(isize_zero, Felt::from(isize_zero).try_into().unwrap()); + let isize_minus_one = -1isize; + assert_eq!( + isize_minus_one, + Felt::from(isize_minus_one).try_into().unwrap() + ); + // i128 + let i128_max = i128::MAX; + assert_eq!(i128_max, Felt::from(i128_max).try_into().unwrap()); + let i128_min = i128::MIN; + assert_eq!(i128_min, Felt::from(i128_min).try_into().unwrap()); + let i128_zero = 0i128; + assert_eq!(i128_zero, Felt::from(i128_zero).try_into().unwrap()); + let i128_minus_one = -1i128; + assert_eq!( + i128_minus_one, + Felt::from(i128_minus_one).try_into().unwrap() + ); + } + + #[test] + fn try_from_fail_for_out_of_boud_values() { + for i in (i8::MAX as i16 + 1)..i16::MAX { + let f = Felt::from(i); + assert!(i8::try_from(f).is_err()); + } + for i in i16::MIN..(i8::MIN as i16 - 1) { + let f = Felt::from(i); + assert!(i8::try_from(f).is_err()); + } + // It would be too expansive to check all values of larger types, + // so we just check some key ones + + let over_i128_max = Felt::from(i128::MAX) + 1; + assert!(i8::try_from(over_i128_max).is_err()); + assert!(i16::try_from(over_i128_max).is_err()); + assert!(i32::try_from(over_i128_max).is_err()); + assert!(i64::try_from(over_i128_max).is_err()); + assert!(isize::try_from(over_i128_max).is_err()); + assert!(i128::try_from(over_i128_max).is_err()); + + let under_i128_min = Felt::from(i128::MIN) - 1; + assert!(i8::try_from(under_i128_min).is_err()); + assert!(i16::try_from(under_i128_min).is_err()); + assert!(i32::try_from(under_i128_min).is_err()); + assert!(i64::try_from(under_i128_min).is_err()); + assert!(isize::try_from(under_i128_min).is_err()); + assert!(i128::try_from(under_i128_min).is_err()); + let under_i128_min = Felt::from(i128::MIN) - 2; + assert!(i8::try_from(under_i128_min).is_err()); + assert!(i16::try_from(under_i128_min).is_err()); + assert!(i32::try_from(under_i128_min).is_err()); + assert!(i64::try_from(under_i128_min).is_err()); + assert!(isize::try_from(under_i128_min).is_err()); + assert!(i128::try_from(under_i128_min).is_err()); + } + + #[test] + fn felt_from_into_bool() { + assert!(Felt::from(true) == Felt::ONE); + assert!(Felt::from(false) == Felt::ZERO); + } +} diff --git a/fuzz/felt/fuzz_targets/conversions.rs b/fuzz/felt/fuzz_targets/conversions.rs index 6b2c740c..2eccc699 100644 --- a/fuzz/felt/fuzz_targets/conversions.rs +++ b/fuzz/felt/fuzz_targets/conversions.rs @@ -75,4 +75,68 @@ fuzz_target!(|felt: Felt| { UnsignedInteger::<4>::from_hex(&felt.to_hex_string()).unwrap(), UnsignedInteger::<4>::from_limbs(le_digits_reversed) ); + + // TryForm unsigned primitives + if felt <= Felt::from(u8::MAX) { + assert_eq!(felt, Felt::from(u8::try_from(felt).unwrap())); + } else { + assert!(u8::try_from(felt).is_err()); + } + if felt <= Felt::from(u16::MAX) { + assert_eq!(felt, Felt::from(u16::try_from(felt).unwrap())); + } else { + assert!(u16::try_from(felt).is_err()); + } + if felt <= Felt::from(u32::MAX) { + assert_eq!(felt, Felt::from(u32::try_from(felt).unwrap())); + } else { + assert!(u32::try_from(felt).is_err()); + } + if felt <= Felt::from(u64::MAX) { + assert_eq!(felt, Felt::from(u64::try_from(felt).unwrap())); + } else { + assert!(u64::try_from(felt).is_err()); + } + if felt <= Felt::from(usize::MAX) { + assert_eq!(felt, Felt::from(usize::try_from(felt).unwrap())); + } else { + assert!(usize::try_from(felt).is_err()); + } + if felt <= Felt::from(u128::MAX) { + assert_eq!(felt, Felt::from(u128::try_from(felt).unwrap())); + } else { + assert!(u128::try_from(felt).is_err()); + } + + // TryFrom signed primitives + if felt <= Felt::from(i8::MAX) || felt >= Felt::from(i8::MIN) { + assert_eq!(felt, Felt::from(i8::try_from(felt).unwrap())); + } else { + assert!(i8::try_from(felt).is_err()); + } + if felt <= Felt::from(i16::MAX) || felt >= Felt::from(i16::MIN) { + assert_eq!(felt, Felt::from(i16::try_from(felt).unwrap())); + } else { + assert!(i16::try_from(felt).is_err()); + } + if felt <= Felt::from(i32::MAX) || felt >= Felt::from(i32::MIN) { + assert_eq!(felt, Felt::from(i32::try_from(felt).unwrap())); + } else { + assert!(i32::try_from(felt).is_err()); + } + if felt <= Felt::from(i64::MAX) || felt >= Felt::from(i64::MIN) { + assert_eq!(felt, Felt::from(i64::try_from(felt).unwrap())); + } else { + assert!(i64::try_from(felt).is_err()); + } + if felt <= Felt::from(isize::MAX) || felt >= Felt::from(isize::MIN) { + assert_eq!(felt, Felt::from(isize::try_from(felt).unwrap())); + } else { + assert!(isize::try_from(felt).is_err()); + } + if felt <= Felt::from(i128::MAX) || felt >= Felt::from(i128::MIN) { + assert_eq!(felt, Felt::from(i128::try_from(felt).unwrap())); + } else { + assert!(i128::try_from(felt).is_err()); + } }); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index be82282a..bd68a076 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] channel = "1.77.2" -components = ["rustfmt", "clippy"] +components = ["rustfmt", "clippy", "rust-analyzer"] profile = "minimal"