diff --git a/Scarb.lock b/Scarb.lock index 1b118f632..e5fc546a6 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -3,8 +3,7 @@ version = 1 [[package]] name = "alexandria_storage" -version = "0.3.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=01a7690dc25d19a086f525b8ce66aa505c8e7527#01a7690dc25d19a086f525b8ce66aa505c8e7527" +version = "0.1.0" [[package]] name = "contracts" diff --git a/crates/alexandria_storage/.gitignore b/crates/alexandria_storage/.gitignore new file mode 100644 index 000000000..eb5a316cb --- /dev/null +++ b/crates/alexandria_storage/.gitignore @@ -0,0 +1 @@ +target diff --git a/crates/alexandria_storage/Scarb.toml b/crates/alexandria_storage/Scarb.toml new file mode 100644 index 000000000..88debce57 --- /dev/null +++ b/crates/alexandria_storage/Scarb.toml @@ -0,0 +1,7 @@ +[package] +name = "alexandria_storage" +version = "0.1.0" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] diff --git a/crates/alexandria_storage/src/lib.cairo b/crates/alexandria_storage/src/lib.cairo new file mode 100644 index 000000000..4ebea4ff9 --- /dev/null +++ b/crates/alexandria_storage/src/lib.cairo @@ -0,0 +1 @@ +mod list; \ No newline at end of file diff --git a/crates/alexandria_storage/src/list.cairo b/crates/alexandria_storage/src/list.cairo new file mode 100644 index 000000000..3876206f5 --- /dev/null +++ b/crates/alexandria_storage/src/list.cairo @@ -0,0 +1,355 @@ +use integer::U32DivRem; +use poseidon::poseidon_hash_span; +use starknet::storage_access::{ + Store, StorageBaseAddress, storage_address_to_felt252, storage_address_from_base, + storage_base_address_from_felt252 +}; +use starknet::{storage_read_syscall, storage_write_syscall, SyscallResult, SyscallResultTrait}; + +const POW2_8: u32 = 256; // 2^8 + +#[derive(Drop)] +struct List { + address_domain: u32, + base: StorageBaseAddress, + len: u32, // number of elements in array + storage_size: u8 +} + +trait ListTrait { + /// Instantiates a new List with the given base address. + /// + /// + /// # Arguments + /// + /// * `address_domain` - The domain of the address. Only address_domain 0 is + /// currently supported, in the future it will enable access to address + /// spaces with different data availability + /// * `base` - The base address of the List. This corresponds to the + /// location in storage of the List's first element. + /// + /// # Returns + /// + /// A new List. + fn new(address_domain: u32, base: StorageBaseAddress) -> List; + + /// Fetches an existing List stored at the given base address. + /// Returns an error if the storage read fails. + /// + /// # Arguments + /// + /// * `address_domain` - The domain of the address. Only address_domain 0 is + /// currently supported, in the future it will enable access to address + /// spaces with different data availability + /// * `base` - The base address of the List. This corresponds to the + /// location in storage of the List's first element. + /// + /// # Returns + /// + /// An instance of the List fetched from storage, or an error in + /// `SyscallResult`. + fn fetch(address_domain: u32, base: StorageBaseAddress) -> SyscallResult>; + + /// Appends an existing Span to a List. Returns an error if the span + /// cannot be appended to the a list due to storage errors + /// + /// # Arguments + /// + /// * `self` - The List to add the span to. + /// * `span` - A Span to append to the List. + /// + /// # Returns + /// + /// A List constructed from the span or an error in `SyscallResult`. + fn append_span(ref self: List, span: Span) -> SyscallResult<()>; + + /// Gets the length of the List. + /// + /// # Returns + /// + /// The number of elements in the List. + fn len(self: @List) -> u32; + + /// Checks if the List is empty. + /// + /// # Returns + /// + /// `true` if the List is empty, `false` otherwise. + fn is_empty(self: @List) -> bool; + + /// Appends a value to the end of the List. Returns an error if the append + /// operation fails due to reasons such as storage issues. + /// + /// # Arguments + /// + /// * `value` - The value to append. + /// + /// # Returns + /// + /// The index at which the value was appended or an error in `SyscallResult`. + fn append(ref self: List, value: T) -> SyscallResult; + + /// Retrieves an element by index from the List. Returns an error if there + /// is a retrieval issue. + /// + /// # Arguments + /// + /// * `index` - The index of the element to retrieve. + /// + /// # Returns + /// + /// An `Option` which is `None` if the list is empty, or + /// `Some(value)` if an element was found, encapsulated + /// in `SyscallResult`. + fn get(self: @List, index: u32) -> SyscallResult>; + + /// Sets the value of an element at a given index. + /// + /// # Arguments + /// + /// * `index` - The index of the element to modify. + /// * `value` - The value to set at the given index. + /// + /// # Returns + /// + /// A result indicating success or encapsulating the error in `SyscallResult`. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. + fn set(ref self: List, index: u32, value: T) -> SyscallResult<()>; + + /// Clears the List by setting its length to 0. + /// + /// The storage is not actually cleared, only the length is set to 0. + /// The values can still be accessible using low-level syscalls, but cannot + /// be accessed through the list interface. + fn clean(ref self: List); + + /// Removes and returns the first element of the List. + /// + /// The storage is not actually cleared, only the length is decreased by + /// one. + /// The value popped can still be accessible using low-level syscalls, but + /// cannot be accessed through the list interface. + /// # Returns + /// + /// An `Option` which is `None` if the index is out of bounds, or + /// `Some(value)` if an element was found at the given index, encapsulated + /// in `SyscallResult`. + fn pop_front(ref self: List) -> SyscallResult>; + + /// Converts the List into an Array. If the list cannot be converted + /// to an array due storage errors, an error is returned. + /// + /// # Returns + /// + /// An `Array` containing all the elements of the List, encapsulated + /// in `SyscallResult`. + fn array(self: @List) -> SyscallResult>; +} + +impl ListImpl, +Drop, +Store> of ListTrait { + #[inline(always)] + fn new(address_domain: u32, base: StorageBaseAddress) -> List { + let storage_size: u8 = Store::::size(); + List { address_domain, base, len: 0, storage_size } + } + + #[inline(always)] + fn fetch(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { + ListStore::read(address_domain, base) + } + + fn append_span(ref self: List, mut span: Span) -> SyscallResult<()> { + let mut index = self.len; + self.len += span.len(); + + loop { + match span.pop_front() { + Option::Some(v) => { + let (base, offset) = calculate_base_and_offset_for_index( + self.base, index, self.storage_size + ); + match Store::write_at_offset(self.address_domain, base, offset, *v) { + Result::Ok(_) => {}, + Result::Err(e) => { break Result::Err(e); } + } + index += 1; + }, + Option::None => { break Store::write(self.address_domain, self.base, self.len); } + }; + } + } + + #[inline(always)] + fn len(self: @List) -> u32 { + *self.len + } + + #[inline(always)] + fn is_empty(self: @List) -> bool { + *self.len == 0 + } + + fn append(ref self: List, value: T) -> SyscallResult { + let (base, offset) = calculate_base_and_offset_for_index( + self.base, self.len, self.storage_size + ); + Store::write_at_offset(self.address_domain, base, offset, value)?; + + let append_at = self.len; + self.len += 1; + Store::write(self.address_domain, self.base, self.len)?; + + Result::Ok(append_at) + } + + fn get(self: @List, index: u32) -> SyscallResult> { + if (index >= *self.len) { + return Result::Ok(Option::None); + } + + let (base, offset) = calculate_base_and_offset_for_index( + *self.base, index, *self.storage_size + ); + let t = Store::read_at_offset(*self.address_domain, base, offset)?; + Result::Ok(Option::Some(t)) + } + + fn set(ref self: List, index: u32, value: T) -> SyscallResult<()> { + assert(index < self.len, 'List index out of bounds'); + let (base, offset) = calculate_base_and_offset_for_index( + self.base, index, self.storage_size + ); + Store::write_at_offset(self.address_domain, base, offset, value) + } + + #[inline(always)] + fn clean(ref self: List) { + self.len = 0; + Store::write(self.address_domain, self.base, self.len); + } + + fn pop_front(ref self: List) -> SyscallResult> { + if self.len == 0 { + return Result::Ok(Option::None); + } + + let popped = self.get(self.len - 1)?; + // not clearing the popped value to save a storage write, + // only decrementing the len - makes it unaccessible through + // the interfaces, next append will overwrite the values + self.len -= 1; + Store::write(self.address_domain, self.base, self.len)?; + + Result::Ok(popped) + } + + fn array(self: @List) -> SyscallResult> { + let mut array = array![]; + let mut index = 0; + let result: SyscallResult<()> = loop { + if index == *self.len { + break Result::Ok(()); + } + let value = match self.get(index) { + Result::Ok(v) => v, + Result::Err(e) => { break Result::Err(e); } + }.expect('List index out of bounds'); + array.append(value); + index += 1; + }; + + match result { + Result::Ok(_) => Result::Ok(array), + Result::Err(e) => Result::Err(e) + } + } +} + +impl AListIndexViewImpl, +Drop, +Store> of IndexView, u32, T> { + fn index(self: @List, index: u32) -> T { + self.get(index).expect('read syscall failed').expect('List index out of bounds') + } +} + +// this functions finds the StorageBaseAddress of a "storage segment" (a continuous space of 256 storage slots) +// and an offset into that segment where a value at `index` is stored +// each segment can hold up to `256 // storage_size` elements +// +// the way how the address is calculated is very similar to how a LegacyHash map works: +// +// first we take the `list_base` address which is derived from the name of the storage variable +// then we hash it with a `key` which is the number of the segment where the element at `index` belongs (from 0 upwards) +// we hash these two values: H(list_base, key) to the the `segment_base` address +// finally, we calculate the offset into this segment, taking into account the size of the elements held in the array +// +// by way of example: +// +// say we have an List and Foo's storage_size is 8 +// struct storage: { +// bar: List +// } +// +// the storage layout would look like this: +// +// segment0: [0..31] - elements at indexes 0 to 31 +// segment1: [32..63] - elements at indexes 32 to 63 +// segment2: [64..95] - elements at indexes 64 to 95 +// etc. +// +// where addresses of each segment are: +// +// segment0 = hash(bar.address(), 0) +// segment1 = hash(bar.address(), 1) +// segment2 = hash(bar.address(), 2) +// +// so for getting a Foo at index 90 this function would return address of segment2 and offset of 26 + +fn calculate_base_and_offset_for_index( + list_base: StorageBaseAddress, index: u32, storage_size: u8 +) -> (StorageBaseAddress, u8) { + let max_elements = POW2_8 / storage_size.into(); + let (key, offset) = U32DivRem::div_rem(index, max_elements.try_into().unwrap()); + + // hash the base address and the key which is the segment number + let addr_elements = array![ + storage_address_to_felt252(storage_address_from_base(list_base)), key.into() + ]; + let segment_base = storage_base_address_from_felt252(poseidon_hash_span(addr_elements.span())); + + (segment_base, offset.try_into().unwrap() * storage_size) +} + +impl ListStore> of Store> { + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { + let len: u32 = Store::read(address_domain, base).unwrap_syscall(); + let storage_size: u8 = Store::::size(); + Result::Ok(List { address_domain, base, len, storage_size }) + } + + #[inline(always)] + fn write(address_domain: u32, base: StorageBaseAddress, value: List) -> SyscallResult<()> { + Store::write(address_domain, base, value.len) + } + + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8 + ) -> SyscallResult> { + let len: u32 = Store::read_at_offset(address_domain, base, offset).unwrap_syscall(); + let storage_size: u8 = Store::::size(); + Result::Ok(List { address_domain, base, len, storage_size }) + } + + #[inline(always)] + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8, value: List + ) -> SyscallResult<()> { + Store::write_at_offset(address_domain, base, offset, value.len) + } + + fn size() -> u8 { + Store::::size() + } +} diff --git a/crates/contracts/Scarb.toml b/crates/contracts/Scarb.toml index ce42c717e..eb1133e04 100644 --- a/crates/contracts/Scarb.toml +++ b/crates/contracts/Scarb.toml @@ -9,7 +9,7 @@ starknet.workspace = true evm = { path = "../evm" } openzeppelin = { path = "../openzeppelin" } utils = { path = "../utils" } -alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev = "01a7690dc25d19a086f525b8ce66aa505c8e7527" } +alexandria_storage = { path = "../alexandria_storage" } [tool] fmt.workspace = true