Skip to content

Commit

Permalink
Introduce a StorageBytes type in the standard library (FuelLabs#4001)
Browse files Browse the repository at this point in the history
## Description

The following allows for the storage of a collection of tightly packed
bytes with the use of the `Bytes` type. This will provide significant
space savings over a `StorageVec<u8>` and will also unblock
`StorageString` in the Sway-libs repo.
Please note however that the bytes are still padded to the size of a
quadword in storage. There will be at most 31 padded bytes for any
`StorageBytes` type.

I've initially opted to omit adding functions such as `push()`. This is
to encourage any byte manipulation to be done in the heap using the
`Bytes` type and prevent successive storage read/writes calls in the
same transaction.

Closes FuelLabs#3910 

## 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 Feb 8, 2023
1 parent b6f19a3 commit 2d2a8e5
Show file tree
Hide file tree
Showing 8 changed files with 353 additions and 4 deletions.
2 changes: 1 addition & 1 deletion sway-lib-std/src/lib.sw
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ dep registers;
dep call_frames;
dep context;
dep hash;
dep r#storage;
dep b512;
dep address;
dep identity;
dep vec;
dep bytes;
dep r#storage;
dep b256;
dep tx;
dep inputs;
Expand Down
95 changes: 92 additions & 3 deletions sway-lib-std/src/storage.sw
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
library r#storage;

use ::alloc::alloc;
use ::alloc::{alloc, realloc_bytes};
use ::assert::assert;
use ::hash::sha256;
use ::option::Option;
use ::bytes::Bytes;

/// Store a stack value in storage. Will not work for heap values.
///
Expand Down Expand Up @@ -108,12 +109,12 @@ pub fn get<T>(key: b256) -> Option<T> {
}
}

/// Clear a sequence of consecutive storage slots starting at a some key. Returns a Boolean
/// Clear a sequence of consecutive 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
/// * `key` - The key of the first storage slot that will be cleared
///
/// ### Examples
///
Expand Down Expand Up @@ -638,3 +639,91 @@ impl<V> StorageVec<V> {
store(__get_storage_key(), 0);
}
}

pub struct StorageBytes {}

impl StorageBytes {
/// Takes a `Bytes` type and stores the underlying collection of tightly packed bytes.
///
/// ### Arguments
///
/// * `bytes` - The bytes which will be stored.
///
/// ### 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(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());
}

/// Constructs a `Bytes` type from a collection of tightly packed bytes in storage.
///
/// ### 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.write_bytes(bytes);
///
/// let retrieved_bytes = storage.stored_bytes.into_bytes(key);
/// 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;

// Load the stores bytes into the `Bytes` type pointer.
let _ = __state_load_quad(sha256(__get_storage_key()), bytes.buf.ptr, number_of_slots);

Option::Some(bytes)
} else {
Option::None
}
}
}
1 change: 1 addition & 0 deletions test/src/sdk-harness/test_projects/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod registers;
mod result_in_abi;
mod script_data;
mod storage;
mod storage_bytes;
mod storage_map;
mod storage_vec;
mod token_ops;
Expand Down
2 changes: 2 additions & 0 deletions test/src/sdk-harness/test_projects/storage_bytes/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
13 changes: 13 additions & 0 deletions test/src/sdk-harness/test_projects/storage_bytes/Forc.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[[package]]
name = 'core'
source = 'path+from-root-B82B1B615F276312'

[[package]]
name = 'std'
source = 'path+from-root-B82B1B615F276312'
dependencies = ['core']

[[package]]
name = 'storage_bytes'
source = 'member'
dependencies = ['std']
8 changes: 8 additions & 0 deletions test/src/sdk-harness/test_projects/storage_bytes/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "storage_bytes"

[dependencies]
std = { path = "../../../../../sway-lib-std" }
186 changes: 186 additions & 0 deletions test/src/sdk-harness/test_projects/storage_bytes/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use fuels::prelude::*;

abigen!(Contract(
name = "TestStorageBytesContract",
abi = "test_projects/storage_bytes/out/debug/storage_bytes-abi.json",
));

async fn setup() -> TestStorageBytesContract {
let wallet = launch_provider_and_get_wallet().await;
let id = Contract::deploy(
"test_projects/storage_bytes/out/debug/storage_bytes.bin",
&wallet,
TxParameters::default(),
StorageConfiguration::with_storage_path(Some(
"test_projects/storage_bytes/out/debug/storage_bytes-storage_slots.json".to_string(),
)),
)
.await
.unwrap();

TestStorageBytesContract::new(id, wallet)
}

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

let input = vec![1u8];

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

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

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

instance
.methods()
.assert_stored_bytes(input)
.call()
.await
.unwrap();
}

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

let input = vec![1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8];

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

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

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

instance.methods().assert_stored_bytes(input).call().await.unwrap();
}

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

let input = vec![
1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 1u8, 2u8,
3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8,
];

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

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

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

instance.methods().assert_stored_bytes(input).call().await.unwrap();
}

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

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

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

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

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

instance
.methods()
.assert_stored_bytes(input.as_bytes().into())
.call()
.await
.unwrap();
}

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

// 2060 bytes
let input = String::from("Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, porttitor ut, iaculis quis, sem. Phasellus rhoncus. Aenean id metus id velit ullamcorper pulvinar. Vestibulum fermentum tortor id mi. Pellentesque ipsum. Nulla non arcu lacinia neque faucibus fringilla. Nulla non lectus sed nisl molestie malesuada. Proin in tellus sit amet nibh dignissim sagittis. Vivamus luctus egestas leo. Maecenas sollicitudin. Nullam rhoncus aliquam metus. Etiam egestas wisi a erat.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. Aliquam erat volutpat. Nunc auctor. Mauris pretium quam et urna. Fusce nibh. Duis risus. Curabitur sagittis hendrerit ante. Aliquam erat volutpat. Vestibulum erat nulla, ullamcorper nec, rutrum non, nonummy ac, erat. Duis condimentum augue id magna semper rutrum. Nullam justo enim, consectetuer nec, ullamcorper ac, vestibulum in, elit. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Fusce consectetuer risus a nunc. Aliquam ornare wisi eu metus. Integer pellentesque quam vel velit. Duis pulvinar.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi gravida libero nec velit. Morbi scelerisque luctus velit. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Proin mattis lacinia justo. Vestibulum facilisis auctor urna. Aliquam in lorem sit amet leo accumsan lacinia. Integer rutrum, orci vestibulum ullamcorper ultricies, lacus quam ultricies odio, vitae placerat pede sem sit amet enim. Phasellus et lorem id felis nonummy placerat. Fusce dui leo, imperdiet in, aliquam sit amet, feugiat eu, orci. Aenean vel massa quis mauris vehicula lacinia. Quisque tincidunt scelerisque libero. Maecenas libero. Etiam dictum tincidunt diam. Donec ipsum massa, ullamcorper in, auctor et, scelerisque sed, est. Suspendisse nisl. Sed convallis magna eu sem. Cras pede libero, dapibus nec, pretium");

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

let tx_params = TxParameters::new(None, Some(12_000_000), None);
instance
.methods()
.store_bytes(input.clone().as_bytes().into())
.tx_params(tx_params)
.call()
.await
.unwrap();

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

let tx_params = TxParameters::new(None, Some(12_000_000), None);
instance
.methods()
.assert_stored_bytes(input.as_bytes().into())
.tx_params(tx_params)
.call()
.await
.unwrap();
}

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

let input1 = String::from("Fuel is the fastest modular execution layer");
let input2 = String::from("Fuel is blazingly fast!");

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

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

instance
.methods()
.assert_stored_bytes(input1.as_bytes().into())
.call()
.await
.unwrap();

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

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

instance
.methods()
.assert_stored_bytes(input2.as_bytes().into())
.call()
.await
.unwrap();
}
Loading

0 comments on commit 2d2a8e5

Please sign in to comment.