generated from eigerco/beerus
-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
<!--- 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
1 parent
fe337ab
commit b5c8356
Showing
6 changed files
with
223 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
mod test_bytes; | ||
mod test_bytes_store; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |