Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/utils/src/storage/tests.cairo
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod test_iterable_map;
mod test_utils;
101 changes: 101 additions & 0 deletions packages/utils/src/storage/tests/test_utils.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use starknet::EthAddress;
use starkware_utils::storage::utils::{Castable160, Castable64};

#[test]
fn test_u8_castable() {
let val: u8 = 10;
let packed = Castable160::encode(val);
let unpacked: u8 = Castable160::decode(packed);
assert(val == unpacked, 'u8 mismatch');
}

#[test]
fn test_u128_castable() {
let val: u128 = 340282366920938463463374607431768211455; // MAX
let packed = Castable160::encode(val);
let unpacked: u128 = Castable160::decode(packed);
assert(val == unpacked, 'u128 mismatch');
}

#[test]
fn test_i8_castable() {
let val: i8 = -10;
let packed = Castable160::encode(val);
let unpacked: i8 = Castable160::decode(packed);
assert(val == unpacked, 'i8 mismatch');

let val: i8 = 127;
let packed = Castable160::encode(val);
let unpacked: i8 = Castable160::decode(packed);
assert(val == unpacked, 'i8 max mismatch');

let val: i8 = -128;
let packed = Castable160::encode(val);
let unpacked: i8 = Castable160::decode(packed);
assert(val == unpacked, 'i8 min mismatch');
}

#[test]
fn test_i128_castable() {
let val: i128 = -170141183460469231731687303715884105728;
let packed = Castable160::encode(val);
let unpacked: i128 = Castable160::decode(packed);
assert(val == unpacked, 'i128 mismatch');
}

#[test]
fn test_eth_address_castable() {
let val: EthAddress = 1252485858049991401322336118102073441816987242963.try_into().unwrap();
let packed = Castable160::encode(val);
let unpacked: EthAddress = Castable160::decode(packed);
assert(val == unpacked, 'EthAddress mismatch');
}

#[test]
#[should_panic(expected: ('Castable160: high bits not 0',))]
fn test_high_bits_panic() {
let packed = (1, 1); // High bit set
let _val: u8 = Castable160::decode(packed);
}

#[test]
fn test_u8_Castable64() {
let val: u8 = 10;
let packed = Castable64::encode(val);
let unpacked: u8 = Castable64::decode(packed);
assert(val == unpacked, 'u8 Castable64 mismatch');
}

#[test]
fn test_u64_Castable64() {
let val: u64 = 18446744073709551615; // MAX
let packed = Castable64::encode(val);
let unpacked: u64 = Castable64::decode(packed);
assert(val == unpacked, 'u64 Castable64 mismatch');
}

#[test]
fn test_i8_Castable64() {
let val: i8 = -10;
let packed = Castable64::encode(val);
let unpacked: i8 = Castable64::decode(packed);
assert(val == unpacked, 'i8 Castable64 mismatch');
}

#[test]
fn test_i64_Castable64() {
let val: i64 = -10;
let packed = Castable64::encode(val);
let unpacked: i64 = Castable64::decode(packed);
assert(val == unpacked, 'i64 Castable64 mismatch');

let val: i64 = 9223372036854775807; // MAX
let packed = Castable64::encode(val);
let unpacked: i64 = Castable64::decode(packed);
assert(val == unpacked, 'i64 max Castable64 mismatch');

let val: i64 = -9223372036854775808; // MIN
let packed = Castable64::encode(val);
let unpacked: i64 = Castable64::decode(packed);
assert(val == unpacked, 'i64 min Castable64 mismatch');
}
154 changes: 153 additions & 1 deletion packages/utils/src/storage/utils.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use starknet::Store;
use starknet::storage::{
Mutable, StorageAsPointer, StoragePointer, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{EthAddress, Store};


pub trait AddToStorage<T> {
type Value;
Expand Down Expand Up @@ -60,3 +61,154 @@ pub impl StoragePathSubFromStorageImpl<
new_value
}
}


/// Trait for types that can be stored as 160 bits.
///
/// The format is a tuple `(low: u128, high: u32)`.
///
/// # Requirements
/// - `encode` should be injective: distinct values map to distinct tuples.
/// - `decode` should be the inverse of `encode` on its image:
/// decode(encode(v)) == v
pub trait Castable160<V> {
/// Convert a value into its 160-bit representation `(low, high)`.
fn encode(value: V) -> (u128, u32);

/// Convert a 160-bit representation back into the original value.
fn decode(value: (u128, u32)) -> V;
}

/// `Castable160` implementation for primitive integer types ('u8', 'u16', 'u32', 'u64' and 'u128'.)
/// that fit entirely in 128 bits.
pub impl PrimitiveCastable160<T, +Into<T, u128>, +TryInto<u128, T>, +Drop<T>> of Castable160<T> {
fn encode(value: T) -> (u128, u32) {
(value.into(), 0)
}
fn decode(value: (u128, u32)) -> T {
let (low, high) = value;
assert(high == 0, 'Castable160: high bits not 0');
low.try_into().unwrap()
}
}

/// Trait to define the offset for signed integer.
trait SignedIntegerOffset<T> {
fn offset() -> felt252;
}

impl I8Offset of SignedIntegerOffset<i8> {
fn offset() -> felt252 {
// 2 ** 7
128
}
}

impl I16Offset of SignedIntegerOffset<i16> {
fn offset() -> felt252 {
// 2** 15
32768
}
}

impl I32Offset of SignedIntegerOffset<i32> {
fn offset() -> felt252 {
// 2** 31
2147483648
}
}

impl I64Offset of SignedIntegerOffset<i64> {
fn offset() -> felt252 {
// 2** 63
9223372036854775808
}
}

impl I128Offset of SignedIntegerOffset<i128> {
fn offset() -> felt252 {
// 2** 127
170141183460469231731687303715884105728
}
}

pub impl SignedIntegerCastable160<
T, +SignedIntegerOffset<T>, +Into<T, felt252>, +TryInto<felt252, T>, +Drop<T>,
> of Castable160<T> {
fn encode(value: T) -> (u128, u32) {
let val_felt: felt252 = value.into();
let offset = SignedIntegerOffset::<T>::offset();
let val_u128: u128 = (val_felt + offset).try_into().unwrap();
(val_u128, 0)
}
fn decode(value: (u128, u32)) -> T {
let (low, high) = value;
assert(high == 0, 'Castable160: high bits not 0');
let val_felt: felt252 = low.into();
let offset = SignedIntegerOffset::<T>::offset();
(val_felt - offset).try_into().unwrap()
}
}

pub impl EthAddressCastable160 of Castable160<EthAddress> {
fn encode(value: EthAddress) -> (u128, u32) {
let felt_val: felt252 = value.into();
let u256_val: u256 = felt_val.into();
let low = u256_val.low;
let high: u32 = u256_val.high.try_into().unwrap();
(low, high)
}
fn decode(value: (u128, u32)) -> EthAddress {
let (low, high) = value;
let u256_val = u256 { low, high: high.into() };
u256_val.try_into().unwrap()
}
}

/// Trait for types that can be stored as 64 bits.
///
/// The format is a `u64`.
///
/// # Requirements
/// - `encode` should be injective: distinct values map to distinct tuples.
/// - `decode` should be the inverse of `encode` on its image:
/// decode(encode(v)) == v
pub trait Castable64<V> {
/// Convert a value into its 64-bit representation.
fn encode(value: V) -> u64;
/// Convert a 64-bit representation back into the original value.
fn decode(value: u64) -> V;
}

pub impl PrimitiveCastable64<T, +Into<T, u64>, +TryInto<u64, T>, +Drop<T>> of Castable64<T> {
fn encode(value: T) -> u64 {
value.into()
}
fn decode(value: u64) -> T {
value.try_into().unwrap()
}
}


/// Trait to check if a type fits in 64 bits.
trait FitsIn64<T> {}
impl I8FitsIn64 of FitsIn64<i8> {}
impl I16FitsIn64 of FitsIn64<i16> {}
impl I32FitsIn64 of FitsIn64<i32> {}
impl I64FitsIn64 of FitsIn64<i64> {}

pub impl SignedIntegerCastable64<
T, +FitsIn64<T>, +SignedIntegerOffset<T>, +Into<T, felt252>, +TryInto<felt252, T>, +Drop<T>,
> of Castable64<T> {
fn encode(value: T) -> u64 {
let val_felt: felt252 = value.into();
let offset = SignedIntegerOffset::<T>::offset();
let val_u64: u64 = (val_felt + offset).try_into().unwrap();
val_u64
}
fn decode(value: u64) -> T {
let val_felt: felt252 = value.into();
let offset = SignedIntegerOffset::<T>::offset();
(val_felt - offset).try_into().unwrap()
}
}