Skip to content

Commit

Permalink
Introduce the StorableSlice trait to the std library (FuelLabs#4302)
Browse files Browse the repository at this point in the history
## Description

This PR introduces a way more generalized way to store heap types into
storage as discussed
[here](FuelLabs#4013) with the
`StorableSlice` trait. The current implementation of the `store` and
`get` functions do not support heap types. With the use of `raw_slice`
any heap types may now be stored.

This PR unlocks FuelLabs/sway-libs#40 which is
required for a number of other issues.

With the introduction of the `StorableSlice` trait, the `StorageBytes`
have been refactored to use it.

This trait could be used to convert a `Vec` to a `StorageVec` in an
efficient manner in another PR. While `Vec` already has the ability to
convert to a `raw_slice`, `Storage_Vec` stores each element using a
unique key created by hashing the index of that element i.e.
sha256(index) in a more linked-list like fashion. This would need to be
changed if this functionality is desired.

**NOTE:** A `from_raw_slice` function has been added to the `Bytes` type
to enable the refactor. This should be removed and replaced once
FuelLabs#3882 is resolved and also uses the
`len_bytes` function introduced in that PR.

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [x] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.

---------

Co-authored-by: bitzoic <[email protected]>
  • Loading branch information
bitzoic and bitzoic authored Mar 20, 2023
1 parent 169ad0b commit 55a4fee
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 50 deletions.
5 changes: 5 additions & 0 deletions sway-lib-core/src/raw_slice.sw
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ impl raw_slice {
pub fn len<T>(self) -> u64 {
__div(into_parts(self).1, __size_of::<T>())
}

/// Returns the number of elements in the slice when the elements are bytes.
pub fn number_of_bytes(self) -> u64 {
into_parts(self).1
}
}
40 changes: 40 additions & 0 deletions sway-lib-std/src/bytes.sw
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,18 @@ impl Bytes {
// clear `other`
other.clear();
}

// Should be remove and replace when https://github.com/FuelLabs/sway/pull/3882 is resovled
pub fn from_raw_slice(slice: raw_slice) -> Self {
let number_of_bytes = slice.number_of_bytes();
Self {
buf: RawBytes {
ptr: slice.ptr(),
cap: number_of_bytes,
},
len: number_of_bytes,
}
}
}

impl core::ops::Eq for Bytes {
Expand All @@ -683,6 +695,13 @@ impl core::ops::Eq for Bytes {
}
}

impl AsRawSlice for Bytes {
/// Returns a raw slice of all of the elements in the vector.
fn as_raw_slice(self) -> raw_slice {
asm(ptr: (self.buf.ptr(), self.len)) { ptr: raw_slice }
}
}

/// Methods for converting between the `Bytes` and the `b256` types.
impl From<b256> for Bytes {
fn from(b: b256) -> Bytes {
Expand Down Expand Up @@ -1116,6 +1135,27 @@ fn test_keccak256() {
assert(keccak256(362268190631264256) == bytes.keccak256());
}

#[test()]
fn test_as_raw_slice() {
let val = 0x3497297632836282349729763283628234972976328362823497297632836282;
let slice_1 = asm(ptr: (__addr_of(val), 32)) { ptr: raw_slice };
let mut bytes = Bytes::from_raw_slice(slice_1);
let slice_2 = bytes.as_raw_slice();
assert(slice_1.ptr() == slice_2.ptr());
assert(slice_1.number_of_bytes() == slice_2.number_of_bytes());
}

// This test will need to be updated once https://github.com/FuelLabs/sway/pull/3882 is resolved
#[test()]
fn test_from_raw_slice() {
let val = 0x3497297632836282349729763283628234972976328362823497297632836282;
let slice_1 = asm(ptr: (__addr_of(val), 32)) { ptr: raw_slice };
let mut bytes = Bytes::from_raw_slice(slice_1);
let slice_2 = bytes.as_raw_slice();
assert(slice_1.ptr() == slice_2.ptr());
assert(slice_1.number_of_bytes() == slice_2.number_of_bytes());
}

#[test]
fn test_from_b256() {
let initial = 0x3333333333333333333333333333333333333333333333333333333333333333;
Expand Down
238 changes: 199 additions & 39 deletions sway-lib-std/src/storage.sw
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
library;

use ::alloc::{alloc, realloc_bytes};
use ::alloc::{alloc, alloc_bytes, realloc_bytes};
use ::assert::assert;
use ::hash::sha256;
use ::option::Option;
Expand Down Expand Up @@ -137,6 +137,119 @@ pub fn clear<T>(key: b256) -> bool {
__state_clear(key, number_of_slots)
}

/// Store a raw_slice from the heap into storage.
///
/// ### Arguments
///
/// * `key` - The storage slot at which the variable will be stored.
/// * `slice` - The raw_slice to be stored.
///
/// ### Examples
///
/// ```sway
/// use std::{alloc::alloc_bytes, storage::{store_slice, get_slice}, constants::ZERO_B256};
///
/// let slice = asm(ptr: (alloc_bytes(1), 1)) { ptr: raw_slice };
/// assert(get_slice(ZERO_B256).is_none());
/// store_slice(ZERO_B256, slice);
/// let stored_slice = get_slice(ZERO_B256).unwrap();
/// assert(slice == stored_slice);
/// ```
#[storage(write)]
pub fn store_slice(key: b256, slice: raw_slice) {
// Get the number of storage slots needed based on the size of bytes.
let number_of_bytes = slice.number_of_bytes();
let number_of_slots = (number_of_bytes + 31) >> 5;
let mut ptr = slice.ptr();

// The capacity needs to be a multiple of 32 bytes so we can
// make the 'quad' storage instruction store without accessing unallocated heap memory.
ptr = realloc_bytes(ptr, number_of_bytes, number_of_slots * 32);

// Store `number_of_slots * 32` bytes starting at storage slot `key`.
let _ = __state_store_quad(sha256(key), ptr, number_of_slots);

// Store the length of the bytes
store(key, number_of_bytes);
}

/// Load a raw_slice from storage.
///
/// If no value was previously stored at `key`, `Option::None` is returned. Otherwise,
/// `Option::Some(value)` is returned, where `value` is the value stored at `key`.
///
/// ### Arguments
///
/// * `key` - The storage slot to load the value from.
///
/// ### Examples
///
/// ```sway
/// use std::{alloc::alloc_bytes, storage::{store_slice, get_slice}, constants::ZERO_B256};
///
/// let slice = asm(ptr: (alloc_bytes(1), 1)) { ptr: raw_slice };
/// assert(get_slice(ZERO_B256).is_none());
/// store_slice(ZERO_B256, slice);
/// let stored_slice = get_slice(ZERO_B256).unwrap();
/// assert(slice == stored_slice);
/// ```
#[storage(read)]
pub fn get_slice(key: b256) -> Option<raw_slice> {
// Get the length of the slice that is stored.
match get::<u64>(key).unwrap_or(0) {
0 => Option::None,
len => {
// Get the number of storage slots needed based on the size.
let number_of_slots = (len + 31) >> 5;
let ptr = alloc_bytes(number_of_slots * 32);
// Load the stored slice into the pointer.
let _ = __state_load_quad(sha256(key), ptr, number_of_slots);
Option::Some(asm(ptr: (ptr, len)) { ptr: raw_slice })
}
}
}

/// Clear a sequence of storage slots starting at a some key. Returns a Boolean
/// indicating whether all of the storage slots cleared were previously set.
///
/// ### Arguments
///
/// * `key` - The key of the first storage slot that will be cleared
///
/// ### Examples
///
/// ```sway
/// use std::{alloc::alloc_bytes, storage::{clear_slice, store_slice, get_slice}, constants::ZERO_B256};
///
/// let slice = asm(ptr: (alloc_bytes(1), 1)) { ptr: raw_slice };
/// store_slice(ZERO_B256, slice);
/// assert(get_slice(ZERO_B256).is_some());
/// let cleared = clear_slice(ZERO_B256);
/// assert(cleared);
/// assert(get_slice(ZERO_B256).is_none());
/// ```
#[storage(read, write)]
pub fn clear_slice(key: b256) -> bool {
// Get the number of storage slots needed based on the ceiling of `len / 32`
let len = get::<u64>(key).unwrap_or(0);
let number_of_slots = (len + 31) >> 5;

// Clear length and `number_of_slots` bytes starting at storage slot `sha256(key)`
let _ = __state_clear(key, 1);
__state_clear(sha256(key), number_of_slots)
}

pub trait StorableSlice<T> {
#[storage(write)]
fn store(self, argument: T);
#[storage(read)]
fn load(self) -> Option<T>;
#[storage(read, write)]
fn clear(self) -> bool;
#[storage(read)]
fn len(self) -> u64;
}

/// A persistent key-value pair mapping struct.
pub struct StorageMap<K, V> {}

Expand Down Expand Up @@ -685,13 +798,17 @@ impl<V> StorageVec<V> {

pub struct StorageBytes {}

impl StorageBytes {
impl StorableSlice<Bytes> for StorageBytes {
/// Takes a `Bytes` type and stores the underlying collection of tightly packed bytes.
///
/// ### Arguments
///
/// * `bytes` - The bytes which will be stored.
///
/// ### Number of Storage Accesses
///
/// * Writes: `2`
///
/// ### Examples
///
/// ```sway
Expand All @@ -705,30 +822,21 @@ impl StorageBytes {
/// bytes.push(7_u8);
/// bytes.push(9_u8);
///
/// storage.stored_bytes.store_bytes(bytes);
/// storage.stored_bytes.store(bytes);
/// }
/// ```
#[storage(write)]
pub fn store_bytes(self, mut bytes: Bytes) {
// Get the number of storage slots needed based on the size of bytes.
let number_of_slots = (bytes.len() + 31) >> 5;

// The bytes capacity needs to be greater than or a multiple of 32 bytes so we can
// make the 'quad' storage instruction store without accessing unallocated heap memory.
if bytes.buf.cap < number_of_slots * 32 {
bytes.buf.ptr = realloc_bytes(bytes.buf.ptr, bytes.buf.cap, number_of_slots * 32);
}

// Store `number_of_slots * 32` bytes starting at storage slot `key`.
let key = sha256(__get_storage_key());
let _ = __state_store_quad(key, bytes.buf.ptr, number_of_slots);

// Store the length of the bytes
store(__get_storage_key(), bytes.len());
fn store(self, bytes: Bytes) {
let key = __get_storage_key();
store_slice(key, bytes.as_raw_slice());
}

/// Constructs a `Bytes` type from a collection of tightly packed bytes in storage.
///
/// ### Number of Storage Accesses
///
/// * Reads: `2`
///
/// ### Examples
///
/// ```sway
Expand All @@ -741,32 +849,84 @@ impl StorageBytes {
/// bytes.push(5_u8);
/// bytes.push(7_u8);
/// bytes.push(9_u8);
/// storage.stored_bytes.write_bytes(bytes);
///
/// let retrieved_bytes = storage.stored_bytes.into_bytes(key);
/// assert(storage.stored_bytes.load(key).is_none());
/// storage.stored_bytes.store(bytes);
/// let retrieved_bytes = storage.stored_bytes.load(key).unwrap();
/// assert(bytes == retrieved_bytes);
/// }
/// ```
#[storage(read)]
pub fn into_bytes(self) -> Option<Bytes> {
// Get the length of the bytes and create a new `Bytes` type on the heap.
let len = get::<u64>(__get_storage_key()).unwrap_or(0);

if len > 0 {
// Get the number of storage slots needed based on the size of bytes.
let number_of_slots = (len + 31) >> 5;

// Create a new bytes type with a capacity that is a multiple of 32 bytes so we can
// make the 'quad' storage instruction read without overflowing.
let mut bytes = Bytes::with_capacity(number_of_slots * 32);
bytes.len = len;
fn load(self) -> Option<Bytes> {
let key = __get_storage_key();
match get_slice(key) {
Option::Some(slice) => {
Option::Some(Bytes::from_raw_slice(slice))
},
Option::None => Option::None,
}
}

// Load the stores bytes into the `Bytes` type pointer.
let _ = __state_load_quad(sha256(__get_storage_key()), bytes.buf.ptr, number_of_slots);
/// Clears a collection of tightly packed bytes in storage.
///
/// ### Number of Storage Accesses
///
/// * Reads: `1`
/// * Clears: `2`
///
/// ### Examples
///
/// ```sway
/// storage {
/// stored_bytes: StorageBytes = StorageBytes {}
/// }
///
/// fn foo() {
/// let mut bytes = Bytes::new();
/// bytes.push(5_u8);
/// bytes.push(7_u8);
/// bytes.push(9_u8);
/// storage.stored_bytes.store(bytes);
///
/// assert(storage.stored_bytes.load(key).is_some());
/// let cleared = storage.stored_bytes.clear();
/// assert(cleared);
/// let retrieved_bytes = storage.stored_bytes.load(key);
/// assert(retrieved_bytes.is_none());
/// }
/// ```
#[storage(read, write)]
fn clear(self) -> bool {
let key = __get_storage_key();
clear_slice(key)
}

Option::Some(bytes)
} else {
Option::None
}
/// Returns the length of tightly packed bytes in storage.
///
/// ### Number of Storage Accesses
///
/// * Reads: `1`
///
/// ### Examples
///
/// ```sway
/// storage {
/// stored_bytes: StorageBytes = StorageBytes {}
/// }
///
/// fn foo() {
/// let mut bytes = Bytes::new();
/// bytes.push(5_u8);
/// bytes.push(7_u8);
/// bytes.push(9_u8);
///
/// assert(storage.stored_bytes.len() == 0)
/// storage.stored_bytes.store(bytes);
/// assert(storage.stored_bytes.len() == 3);
/// }
/// ```
#[storage(read)]
fn len(self) -> u64 {
get::<u64>(__get_storage_key()).unwrap_or(0)
}
}
31 changes: 31 additions & 0 deletions test/src/sdk-harness/test_projects/storage_bytes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,34 @@ async fn stores_string_twice() {
.await
.unwrap();
}

#[tokio::test]
async fn clears_bytes() {
let instance = setup().await;

let input = String::from("Fuel is blazingly fast!");

instance
.methods()
.store_bytes(input.clone().as_bytes().into())
.call()
.await
.unwrap();

assert_eq!(
instance.methods().len().call().await.unwrap().value,
input.as_bytes().len() as u64
);

assert!(
instance
.methods()
.clear_stored_bytes()
.call()
.await
.unwrap()
.value
);

assert_eq!(instance.methods().len().call().await.unwrap().value, 0);
}
Loading

0 comments on commit 55a4fee

Please sign in to comment.