From 39ccd99b1c017ba1c6b53f6d74775f5ab0054735 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 28 Mar 2024 13:59:55 -0500 Subject: [PATCH] feat: Add store implementation for bytes type; includes tests --- src/bytes/Scarb.toml | 3 +- src/bytes/src/lib.cairo | 2 + src/bytes/src/storage.cairo | 147 +++++++++++++++++++++ src/bytes/src/tests.cairo | 1 + src/bytes/src/tests/test_bytes_store.cairo | 70 ++++++++++ 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/bytes/src/storage.cairo create mode 100644 src/bytes/src/tests/test_bytes_store.cairo diff --git a/src/bytes/Scarb.toml b/src/bytes/Scarb.toml index 3059a36e..bdc59151 100644 --- a/src/bytes/Scarb.toml +++ b/src/bytes/Scarb.toml @@ -9,4 +9,5 @@ fmt.workspace = true [dependencies] alexandria_math = { path = "../math" } -alexandria_data_structures = { path = "../data_structures" } \ No newline at end of file +alexandria_data_structures = { path = "../data_structures" } +starknet.workspace = true diff --git a/src/bytes/src/lib.cairo b/src/bytes/src/lib.cairo index be3854a4..f7f1e7ff 100644 --- a/src/bytes/src/lib.cairo +++ b/src/bytes/src/lib.cairo @@ -1,7 +1,9 @@ mod bytes; +mod storage; #[cfg(test)] mod tests; mod utils; use bytes::{Bytes, BytesTrait}; +use storage::BytesStore; diff --git a/src/bytes/src/storage.cairo b/src/bytes/src/storage.cairo new file mode 100644 index 00000000..4c2ee664 --- /dev/null +++ b/src/bytes/src/storage.cairo @@ -0,0 +1,147 @@ +use alexandria_bytes::bytes::{Bytes, BytesTrait, BYTES_PER_ELEMENT}; +use starknet::SyscallResult; +use starknet::storage_access::{ + Store, StorageAddress, StorageBaseAddress, storage_address_from_base, + storage_base_address_from_felt252, storage_address_from_base_and_offset +}; + +/// Store for a `Bytes` object. +/// +/// The layout of a `Bytes` object in storage is as follows: +/// * Only the size in bytes is stored in the original address where the +/// bytes object is stored. +/// * The actual data is stored in chunks of 256 `u128` values in another location +/// in storage determined by the hash of: +/// - The address storing the size of the bytes object. +/// - The chunk index. +/// - The short string `Bytes`. +impl BytesStore of Store { + #[inline(always)] + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult { + inner_read_bytes(address_domain, storage_address_from_base(base)) + } + #[inline(always)] + fn write(address_domain: u32, base: StorageBaseAddress, value: Bytes) -> SyscallResult<()> { + inner_write_bytes(address_domain, storage_address_from_base(base), value) + } + #[inline(always)] + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8 + ) -> SyscallResult { + inner_read_bytes(address_domain, storage_address_from_base_and_offset(base, offset)) + } + #[inline(always)] + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8, value: Bytes + ) -> SyscallResult<()> { + inner_write_bytes(address_domain, storage_address_from_base_and_offset(base, offset), value) + } + #[inline(always)] + fn size() -> u8 { + 1 + } +} + +/// Returns a pointer to the `chunk`'th of the Bytes object at `address`. +/// The pointer is the `Poseidon` hash of: +/// * `address` - The address of the Bytes object (where the size is stored). +/// * `chunk` - The index of the chunk. +/// * The short string `Bytes` is used as the capacity argument of the sponge +/// construction (domain separation). +fn inner_bytes_pointer(address: StorageAddress, chunk: felt252) -> StorageBaseAddress { + let (r, _, _) = core::poseidon::hades_permutation(address.into(), chunk, 'Bytes'_felt252); + storage_base_address_from_felt252(r) +} + +/// Reads a bytes from storage from domain `address_domain` and address `address`. +/// The length of the bytes is read from `address` at domain `address_domain`. +/// For more info read the documentation of `BytesStore`. +fn inner_read_bytes(address_domain: u32, address: StorageAddress) -> SyscallResult { + let size: usize = + match starknet::syscalls::storage_read_syscall(address_domain, address)?.try_into() { + Option::Some(x) => x, + Option::None => { return SyscallResult::Err(array!['Invalid Bytes size']); }, + }; + let (mut remaining_full_words, last_word_len) = core::DivRem::div_rem( + size, BYTES_PER_ELEMENT.try_into().unwrap() + ); + let mut chunk = 0; + let mut chunk_base = inner_bytes_pointer(address, chunk); + let mut index_in_chunk = 0_u8; + let mut result: Bytes = BytesTrait::new_empty(); + loop { + if remaining_full_words == 0 { + break Result::Ok(()); + } + let value = + match starknet::syscalls::storage_read_syscall( + address_domain, storage_address_from_base_and_offset(chunk_base, index_in_chunk) + ) { + Result::Ok(value) => value, + Result::Err(err) => { break Result::Err(err); }, + }; + let value: u128 = match value.try_into() { + Option::Some(x) => x, + Option::None => { break Result::Err(array!['Invalid inner value']); }, + }; + result.data.append(value); + remaining_full_words -= 1; + index_in_chunk = match core::integer::u8_overflowing_add(index_in_chunk, 1) { + Result::Ok(x) => x, + Result::Err(_) => { + // After reading 256 `uint128`s `index_in_chunk` will overflow and we move to the + // next chunk. + chunk += 1; + chunk_base = inner_bytes_pointer(address, chunk); + 0 + }, + }; + }?; + if last_word_len != 0 { + let last_word = starknet::syscalls::storage_read_syscall( + address_domain, storage_address_from_base_and_offset(chunk_base, index_in_chunk) + )?; + result.data.append(last_word.try_into().expect('Invalid last word')); + } + result.size = size; + Result::Ok(result) +} + +/// Writes a bytes to storage at domain `address_domain` and address `address`. +/// The length of the bytes is written to `address` at domain `address_domain`. +/// For more info read the documentation of `BytesStore`. +fn inner_write_bytes( + address_domain: u32, address: StorageAddress, value: Bytes +) -> SyscallResult<()> { + let size = value.size; + starknet::syscalls::storage_write_syscall(address_domain, address, size.into())?; + let mut words = value.data.span(); + let mut chunk = 0; + let mut chunk_base = inner_bytes_pointer(address, chunk); + let mut index_in_chunk = 0_u8; + loop { + let curr_value = match words.pop_front() { + Option::Some(x) => x, + Option::None => { break Result::Ok(()); }, + }; + match starknet::syscalls::storage_write_syscall( + address_domain, + storage_address_from_base_and_offset(chunk_base, index_in_chunk), + (*curr_value).into() + ) { + Result::Ok(_) => {}, + Result::Err(err) => { break Result::Err(err); }, + }; + index_in_chunk = match core::integer::u8_overflowing_add(index_in_chunk, 1) { + Result::Ok(x) => x, + Result::Err(_) => { + // After writing 256 `uint128`s `index_in_chunk` will overflow and we move to the + // next chunk. + chunk += 1; + chunk_base = inner_bytes_pointer(address, chunk); + 0 + }, + }; + }?; + Result::Ok(()) +} diff --git a/src/bytes/src/tests.cairo b/src/bytes/src/tests.cairo index c2bb5452..d5b098d2 100644 --- a/src/bytes/src/tests.cairo +++ b/src/bytes/src/tests.cairo @@ -1 +1,2 @@ mod test_bytes; +mod test_bytes_store; diff --git a/src/bytes/src/tests/test_bytes_store.cairo b/src/bytes/src/tests/test_bytes_store.cairo new file mode 100644 index 00000000..8bf91da6 --- /dev/null +++ b/src/bytes/src/tests/test_bytes_store.cairo @@ -0,0 +1,70 @@ +use alexandria_bytes::Bytes; + +#[starknet::interface] +trait IABytesStore { + fn get_bytes(self: @TContractState) -> Bytes; + fn set_bytes(ref self: TContractState, bytes: Bytes); +} + +#[starknet::contract] +mod ABytesStore { + use alexandria_bytes::{Bytes, BytesStore}; + + #[storage] + struct Storage { + bytes: Bytes, + } + + #[abi(embed_v0)] + impl ABytesStoreImpl of super::IABytesStore { + fn get_bytes(self: @ContractState) -> Bytes { + self.bytes.read() + } + + fn set_bytes(ref self: ContractState, bytes: Bytes) { + self.bytes.write(bytes); + } + } +} + +#[cfg(test)] +mod tests { + use alexandria_bytes::utils::{BytesDebug, BytesDisplay}; + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use starknet::{ClassHash, ContractAddress, deploy_syscall, SyscallResultTrait,}; + use super::{ABytesStore, IABytesStoreDispatcher, IABytesStoreDispatcherTrait}; + + fn deploy() -> IABytesStoreDispatcher { + let class_hash: ClassHash = ABytesStore::TEST_CLASS_HASH.try_into().unwrap(); + let ctor_data: Array = Default::default(); + let (addr, _) = deploy_syscall(class_hash, 0, ctor_data.span(), false).unwrap_syscall(); + IABytesStoreDispatcher { contract_address: addr } + } + + #[test] + fn test_deploy() { + let contract = deploy(); + assert_eq!(contract.get_bytes(), BytesTrait::new_empty(), "Initial bytes should be empty"); + } + + #[test] + fn test_bytes_storage_32_bytes() { + let contract = deploy(); + let bytes = BytesTrait::new(32, array![0x01020304050607080910, 0x11121314151617181920]); + contract.set_bytes(bytes.clone()); + assert_eq!(contract.get_bytes(), bytes, "Bytes should be set correctly"); + } + + #[test] + fn test_bytes_storage_40_bytes() { + let contract = deploy(); + let bytes = BytesTrait::new( + 40, + array![ + 0x01020304050607080910, 0x11121314151617181920, 0x21222324252627280000000000000000 + ] + ); + contract.set_bytes(bytes.clone()); + assert_eq!(contract.get_bytes(), bytes, "Bytes should be set correctly"); + } +}