Skip to content

Commit

Permalink
feat: Bytes Store (#288)
Browse files Browse the repository at this point in the history
<!--- Please provide a general summary of your changes in the title
above -->
Add `Store` implementation for the `Bytes` type to allow using `Bytes`
in contract storage. Also added test cases.

## Pull Request type

<!-- Please try to limit your pull request to one type; submit multiple
pull requests if needed. -->

Please check the type of change your PR introduces:

- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build-related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## What is the current behavior?

<!-- Please describe the current behavior that you are modifying, or
link to a relevant issue. -->
Currently, when trying to use `Bytes` in contract storage you get errors
like this :
```
starknet::Store::<Bytes>::write(
                          ^***^
Error: Failed to compile test artifact, for detailed information go through the logs above
```

Issue Number: N/A

## What is the new behavior?

<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Add storage functions to allow using `Bytes` in storage
- `Bytes` stored in the following format :
- 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"

I used a very similar implementation to the `ByteArray` storage
functions from
[cairo/corelib](https://github.com/starkware-libs/cairo/blob/d090fec472ee3fc40f04b1a19bc362e7bb18e219/corelib/src/starknet/storage_access.cairo#L677)

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this does introduce a breaking change, please describe the
impact and migration path for existing applications below. -->

## Other information

<!-- Any other information that is important to this PR, such as
screenshots of how the component looks before and after the change. -->

This would be useful since it is a common pattern in smart contract
programming to use storage for accessing values between requests/calls
and callbacks.

For example, this would help a lot in implementing `succinct-starknet`,
where a `Bytes` output is [passed to a callback through
storage](https://github.com/succinctlabs/succinctx/blob/9df6a9db651507d60ffa2d75eda3fe526d13f90a/contracts/src/SuccinctGateway.sol#L272).
  • Loading branch information
b-j-roberts authored Apr 17, 2024
1 parent fe337ab commit b5c8356
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 2 deletions.
3 changes: 2 additions & 1 deletion src/bytes/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,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: 1 addition & 1 deletion src/bytes/src/bytes.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use core::byte_array::ByteArrayTrait;
use starknet::ContractAddress;

/// Bytes is a dynamic array of u128, where each element contains 16 bytes.
const BYTES_PER_ELEMENT: usize = 16;
pub const BYTES_PER_ELEMENT: usize = 16;

/// Note that: In Bytes, there are many variables about size and length.
/// We use size to represent the number of bytes in Bytes.
Expand Down
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 @@
pub mod bytes;
pub mod storage;

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

pub use bytes::{Bytes, BytesTrait, BytesIndex};
pub use storage::BytesStore;
146 changes: 146 additions & 0 deletions src/bytes/src/storage.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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`.
pub 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');
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) = 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 data: Array<u128> = array![];
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']); },
};
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)
)?;
data.append(last_word.try_into().expect('Invalid last word'));
}
Result::Ok(BytesTrait::new(size, data))
}

/// 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;
71 changes: 71 additions & 0 deletions src/bytes/src/tests/test_bytes_store.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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::syscalls::deploy_syscall;
use starknet::{ClassHash, ContractAddress, 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 b5c8356

Please sign in to comment.