Skip to content

Commit

Permalink
feat: Add store implementation for bytes type; includes tests
Browse files Browse the repository at this point in the history
  • Loading branch information
b-j-roberts committed Mar 28, 2024
1 parent bbc010b commit 39ccd99
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/bytes/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ fmt.workspace = true

[dependencies]
alexandria_math = { path = "../math" }
alexandria_data_structures = { path = "../data_structures" }
alexandria_data_structures = { path = "../data_structures" }
starknet.workspace = true
2 changes: 2 additions & 0 deletions src/bytes/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod bytes;
mod storage;

#[cfg(test)]
mod tests;
mod utils;

use bytes::{Bytes, BytesTrait};
use storage::BytesStore;
147 changes: 147 additions & 0 deletions src/bytes/src/storage.cairo
Original file line number Diff line number Diff line change
@@ -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<Bytes> {
#[inline(always)]
fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult<Bytes> {
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<Bytes> {
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<Bytes> {
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(())
}
1 change: 1 addition & 0 deletions src/bytes/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod test_bytes;
mod test_bytes_store;
70 changes: 70 additions & 0 deletions src/bytes/src/tests/test_bytes_store.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use alexandria_bytes::Bytes;

#[starknet::interface]
trait IABytesStore<TContractState> {
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<ContractState> {
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<felt252> = 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");
}
}

0 comments on commit 39ccd99

Please sign in to comment.