Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use Vec of Bytes for Contract State #671

Merged
merged 35 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
739f5eb
Change ContractsState::Value to Vec<u8>
bvrooman Feb 1, 2024
29c5a48
WIP
bvrooman Feb 2, 2024
b174897
WIP
bvrooman Feb 5, 2024
6566120
WIP
bvrooman Feb 5, 2024
c2a7644
WIP
bvrooman Feb 6, 2024
f9fe1cf
Update
bvrooman Feb 6, 2024
46388b3
Update CHANGELOG.md
bvrooman Feb 7, 2024
0775951
Fix test
bvrooman Feb 7, 2024
4fc8288
Update blockchain.rs
bvrooman Feb 7, 2024
9f6e7bb
no_std vec
bvrooman Feb 7, 2024
6150b1d
Merge branch 'master' into bvrooman/feat/dynamic-contract-state
Feb 12, 2024
db04050
WIP
bvrooman Feb 16, 2024
99c5541
Test Fix
bvrooman Feb 16, 2024
e5cc6f4
WIP
bvrooman Feb 16, 2024
ce7bbd8
Maintain serialization/deserialization format
bvrooman Feb 19, 2024
0d814ed
Remove dbg
bvrooman Feb 19, 2024
2450866
Merge branch 'master' into bvrooman/feat/dynamic-contract-state
bvrooman Feb 19, 2024
8ff5159
Fix includes for no_std
bvrooman Feb 19, 2024
1c0439d
Merge branch 'master' into bvrooman/feat/dynamic-contract-state
bvrooman Feb 22, 2024
ecfcdc3
Use StorageRead/StorageWrite
bvrooman Feb 23, 2024
a9149f1
Clippeehee
bvrooman Feb 23, 2024
8eda94e
Uncomment take
bvrooman Feb 23, 2024
873b518
Update tests
bvrooman Feb 26, 2024
87b707f
Fix test
bvrooman Feb 26, 2024
df88364
Rename StorageData to ContractsStateData for clarity
bvrooman Feb 26, 2024
b7c1f90
Revert changes to StorageSlot
bvrooman Feb 26, 2024
59bb82b
Merge branch 'master' into bvrooman/feat/dynamic-contract-state
Feb 26, 2024
15a069a
Refactor ContractsState into separate file
bvrooman Feb 26, 2024
65ac84f
Minor refactor
bvrooman Feb 26, 2024
6d7d036
Merge branch 'bvrooman/feat/dynamic-contract-state' of https://github…
bvrooman Feb 26, 2024
f6f3dfd
Update use statements
bvrooman Feb 26, 2024
d444d20
Remove commented out code
bvrooman Feb 26, 2024
bc0a256
Fix feature flag
bvrooman Feb 26, 2024
6dde649
Merge branch 'master' into bvrooman/feat/dynamic-contract-state
bvrooman Feb 26, 2024
d784d40
Use iterator instead of the vector
xgreenx Feb 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Changed
#### Breaking

- [#671](https://github.com/FuelLabs/fuel-vm/pull/671): Support dynamically sized Contract state by changing the storage slot data type to a vector of bytes (`Vec<u8>`).

## [Version 0.45.0]

### Changed
Expand Down
27 changes: 9 additions & 18 deletions fuel-tx/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ use fuel_types::{
};

use alloc::vec::Vec;
use core::{
iter,
ops::Deref,
};
use core::iter;

/// The target size of Merkle tree leaves in bytes. Contract code will will be divided
/// into chunks of this size and pushed to the Merkle tree.
Expand Down Expand Up @@ -105,13 +102,10 @@ impl Contract {
where
I: Iterator<Item = &'a StorageSlot>,
{
let root = SparseMerkleTree::root_from_set(storage_slots.map(|slot| {
(
MerkleTreeKey::new(*slot.key().deref()),
*slot.value().deref(),
)
}));

let storage_slots = storage_slots
.map(|slot| (*slot.key(), slot.value()))
.map(|(key, data)| (MerkleTreeKey::new(key), data));
let root = SparseMerkleTree::root_from_set(storage_slots);
root.into()
}

Expand Down Expand Up @@ -185,10 +179,8 @@ impl TryFrom<&Transaction> for Contract {
#[cfg(test)]
mod tests {
use super::*;
use fuel_types::{
bytes::WORD_SIZE,
Bytes64,
};
use crate::StorageData;
use fuel_types::bytes::WORD_SIZE;
use itertools::Itertools;
use quickcheck_macros::quickcheck;
use rand::{
Expand Down Expand Up @@ -237,9 +229,8 @@ mod tests {

#[rstest]
fn state_root_snapshot(
#[values(Vec::new(), vec![Bytes64::new([1u8; 64])])] state_slot_bytes: Vec<
Bytes64,
>,
#[values(Vec::new(), vec![ (Bytes32::new([1u8; 32]), vec![1u8; 32]) ] )]
state_slot_bytes: Vec<(Bytes32, StorageData)>,
) {
let slots: Vec<StorageSlot> =
state_slot_bytes.iter().map(Into::into).collect_vec();
Expand Down
1 change: 1 addition & 0 deletions fuel-tx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub use transaction::{
PredicateParameters,
Script,
ScriptParameters,
StorageData,
StorageSlot,
Transaction,
TransactionFee,
Expand Down
13 changes: 8 additions & 5 deletions fuel-tx/src/tests/offset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use rand::{
Rng,
SeedableRng,
};
use std::mem::size_of;

// Assert everything is tested. If some of these bools fails, just increase the number of
// cases
Expand Down Expand Up @@ -356,11 +357,13 @@ fn tx_offset_create() {
.storage_slots_offset_at(idx)
.expect("tx with slots contains offsets");

let bytes =
Bytes64::from_bytes_ref_checked(&bytes[ofs..ofs + Bytes64::LEN])
.unwrap();

let slot_p = StorageSlot::from(bytes);
let key_range = ofs..ofs + Bytes32::LEN;
let key = Bytes32::from_bytes_ref_checked(&bytes[key_range.clone()])
.unwrap();
let value_range_start = key_range.end + size_of::<u64>();
let value_range = value_range_start..value_range_start + Bytes32::LEN;
let value = bytes[value_range].to_vec();
let slot_p = StorageSlot::from(&(*key, value));

assert_eq!(slot, &slot_p);
});
Expand Down
3 changes: 2 additions & 1 deletion fuel-tx/src/transaction/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,12 @@ mod tests {
}

fn invert_storage_slot(storage_slot: &mut StorageSlot) {
let mut data = [0u8; 64];
let mut data = [0u8; 64 + 8];
storage_slot
.encode(&mut &mut data[..])
.expect("Failed to encode storage slot");
invert(&mut data);
invert(&mut data[32..40]);
*storage_slot =
StorageSlot::from_bytes(&data).expect("Failed to decode storage slot");
}
Expand Down
5 changes: 4 additions & 1 deletion fuel-tx/src/transaction/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ mod witness;
pub use create::Create;
pub use mint::Mint;
pub use script::Script;
pub use storage::StorageSlot;
pub use storage::{
StorageData,
StorageSlot,
};
pub use utxo_id::UtxoId;
pub use witness::Witness;

Expand Down
24 changes: 16 additions & 8 deletions fuel-tx/src/transaction/types/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,13 @@ mod field {

fn storage_slots_offset_at(&self, idx: usize) -> Option<usize> {
if idx < self.storage_slots.len() {
Some(self.storage_slots_offset() + idx * StorageSlot::SLOT_SIZE)
let storage_slots_size: usize = self
.storage_slots
.iter()
.take(idx)
.map(|slot| slot.size())
.sum();
Some(self.storage_slots_offset() + storage_slots_size)
} else {
None
}
Expand All @@ -464,8 +470,9 @@ mod field {

#[inline(always)]
fn inputs_offset(&self) -> usize {
self.storage_slots_offset()
+ self.storage_slots.len() * StorageSlot::SLOT_SIZE
let storage_slots_size: usize =
self.storage_slots.iter().map(|slot| slot.size()).sum();
self.storage_slots_offset() + storage_slots_size
}

#[inline(always)]
Expand Down Expand Up @@ -636,12 +643,13 @@ mod tests {
#[test]
fn storage_slots_sorting() {
// Test that storage slots must be sorted correctly
let mut slot_data = [0u8; 64];
let mut slot_data = ([0u8; 32], vec![0u8; 32]);

let storage_slots = (0..10u64)
.map(|i| {
slot_data[..8].copy_from_slice(&i.to_be_bytes());
StorageSlot::from(&slot_data.into())
slot_data.0[..8].copy_from_slice(&i.to_be_bytes());
let (key, value) = slot_data.clone();
StorageSlot::from(&(key.into(), value))
})
.collect::<Vec<StorageSlot>>();

Expand All @@ -664,8 +672,8 @@ mod tests {
#[test]
fn storage_slots_no_duplicates() {
let storage_slots = vec![
StorageSlot::new(Bytes32::zeroed(), Bytes32::zeroed()),
StorageSlot::new(Bytes32::zeroed(), Bytes32::zeroed()),
StorageSlot::new(Bytes32::zeroed(), Default::default()),
StorageSlot::new(Bytes32::zeroed(), Default::default()),
];

let err = crate::TransactionBuilder::create(
Expand Down
5 changes: 3 additions & 2 deletions fuel-tx/src/transaction/types/create/ser_de_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::StorageData;
use fuel_types::{
canonical::{
Deserialize,
Expand All @@ -12,8 +13,8 @@ use super::*;
fn test_create_serialization() {
let create = Create {
storage_slots: vec![
StorageSlot::new(Bytes32::from([1u8; 32]), Bytes32::from([2u8; 32])),
StorageSlot::new(Bytes32::from([3u8; 32]), Bytes32::from([4u8; 32])),
StorageSlot::new(Bytes32::from([1u8; 32]), StorageData::from([2u8; 32])),
StorageSlot::new(Bytes32::from([3u8; 32]), StorageData::from([4u8; 32])),
],

..Default::default()
Expand Down
80 changes: 55 additions & 25 deletions fuel-tx/src/transaction/types/storage.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use alloc::vec::Vec;
use fuel_types::{
canonical::{
Deserialize,
Serialize,
},
Bytes32,
Bytes64,
};

#[cfg(feature = "random")]
Expand All @@ -18,49 +18,38 @@ use rand::{

use core::cmp::Ordering;

pub type StorageData = Vec<u8>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about the naming of this. Are we intending all storage to move to Vec or just for contract state? Or is there no distinction between storage and contract storage?

Just trying to understand the scope of all this.

Copy link
Contributor Author

@bvrooman bvrooman Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for the ContractsState table storage. This can be named better - something like ContractsStateData could be more congruent with the table, where the key is ContractsStateKey. I will make that change, but let me know if you have a suggestion you prefer.

Copy link
Contributor Author

@bvrooman bvrooman Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed it to ContractsStateData to illustrate that this is part of the ContractsState table alongside the ContractsStateKey.


#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "typescript", wasm_bindgen::prelude::wasm_bindgen)]
#[derive(Deserialize, Serialize)]
pub struct StorageSlot {
key: Bytes32,
value: Bytes32,
value: StorageData,
}

impl StorageSlot {
pub const SLOT_SIZE: usize = Bytes32::LEN + Bytes32::LEN;

pub const fn new(key: Bytes32, value: Bytes32) -> Self {
pub const fn new(key: Bytes32, value: StorageData) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't add support for dynamic storage in the current PR. We only want to use Vec<u8> in the bytes. This change affects the layout of the serialized transaction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR now includes manual implementations of serialization/deserialization to maintain a fixed size for storage slots. These implementations will be removed when fully supporting dynamic storage.

StorageSlot { key, value }
}

pub const fn key(&self) -> &Bytes32 {
&self.key
}

pub const fn value(&self) -> &Bytes32 {
pub const fn value(&self) -> &StorageData {
&self.value
}
}

impl From<&StorageSlot> for Bytes64 {
fn from(s: &StorageSlot) -> Self {
let mut buf = [0u8; StorageSlot::SLOT_SIZE];

buf[..Bytes32::LEN].copy_from_slice(s.key.as_ref());
buf[Bytes32::LEN..].copy_from_slice(s.value.as_ref());

buf.into()
pub fn size(&self) -> usize {
Serialize::size(self)
}
}

impl From<&Bytes64> for StorageSlot {
fn from(b: &Bytes64) -> Self {
let key = <Bytes32 as Deserialize>::from_bytes(&b[..Bytes32::LEN])
.expect("Infallible deserialization");
let value = <Bytes32 as Deserialize>::from_bytes(&b[Bytes32::LEN..])
.expect("Infallible deserialization");
Self::new(key, value)
impl From<&(Bytes32, StorageData)> for StorageSlot {
fn from((key, value): &(Bytes32, StorageData)) -> Self {
Self::new(*key, value.clone())
}
}

Expand All @@ -69,7 +58,7 @@ impl Distribution<StorageSlot> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> StorageSlot {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: now sample doesn't generate from all possible values anymore, which might affect test coverage. Seems to be a non-issue for now.

StorageSlot {
key: rng.gen(),
value: rng.gen(),
value: rng.gen::<Bytes32>().to_vec(),
}
}
}
Expand All @@ -89,7 +78,10 @@ impl Ord for StorageSlot {
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand::{
RngCore,
SeedableRng,
};
use std::{
fs::File,
path::PathBuf,
Expand All @@ -101,7 +93,7 @@ mod tests {
fn test_storage_slot_serialization() {
let rng = &mut rand::rngs::StdRng::seed_from_u64(8586);
let key: Bytes32 = rng.gen();
let value: Bytes32 = rng.gen();
let value = rng.gen::<Bytes32>().to_vec();

let slot = StorageSlot::new(key, value);
let slots = vec![slot.clone()];
Expand All @@ -124,4 +116,42 @@ mod tests {
serde_json::from_reader(storage_slots_file).expect("read file");
assert_eq!(storage_slots.len(), 1);
}

#[test]
fn test_storage_slot_canonical_serialization() {
Copy link
Member

@MitchTurner MitchTurner Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you include given/when/then sections for these tests?

let rng = &mut rand::rngs::StdRng::seed_from_u64(8586);
let key: Bytes32 = rng.gen();
let mut value = [0u8; 128];
rng.fill_bytes(&mut value);

let slot = StorageSlot::new(key, value.to_vec());

let slot_bytes = slot.to_bytes();

let (slot_key, slot_data) = slot_bytes.split_at(32);

assert_eq!(slot_key, key.as_ref());

let slot_data_num_bytes =
u64::from_bytes(&slot_data[..8]).expect("read from bytes");
assert_eq!(slot_data_num_bytes, 128);

// `from_bytes` works
let recreated_slot =
StorageSlot::from_bytes(&slot_bytes).expect("read from bytes");
assert_eq!(recreated_slot, slot);
}

#[test]
fn test_storage_slot_size() {
let rng = &mut rand::rngs::StdRng::seed_from_u64(8586);
let key: Bytes32 = rng.gen();
let mut value = [0u8; 128];
rng.fill_bytes(&mut value);

let slot = StorageSlot::new(key, value.to_vec());
let size = slot.size();
let expected_size = 32 + 8 + 128; // Key + u64 (data size) + Data
assert_eq!(size, expected_size);
}
}
13 changes: 8 additions & 5 deletions fuel-vm/src/interpreter/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ use crate::{
InterpreterStorage,
},
};
use alloc::vec::Vec;
use alloc::{
vec,
vec::Vec,
};
use fuel_asm::PanicReason;
use fuel_storage::StorageSize;
use fuel_tx::{
Expand Down Expand Up @@ -1041,7 +1044,7 @@ pub(crate) fn state_write_word<S: InterpreterStorage>(
let contract = ContractId::from_bytes_ref(contract.read(memory));
let key = Bytes32::from_bytes_ref(key.read(memory));

let mut value = Bytes32::default();
let mut value = vec![0; 32];

value[..WORD_SIZE].copy_from_slice(&c.to_be_bytes());

Expand Down Expand Up @@ -1228,10 +1231,10 @@ fn state_read_qword<S: InterpreterStorage>(
.map_err(RuntimeError::Storage)?
.into_iter()
.flat_map(|bytes| match bytes {
Some(bytes) => **bytes,
Some(bytes) => bytes.into_owned(),
None => {
all_set = false;
*Bytes32::zeroed()
vec![0; 32]
}
})
.collect();
Expand Down Expand Up @@ -1295,7 +1298,7 @@ fn state_write_qword<'vm, S: InterpreterStorage>(

let values: Vec<_> = memory[input.source_address_memory_range.usizes()]
.chunks_exact(Bytes32::LEN)
.flat_map(|chunk| Some(Bytes32::from(<[u8; 32]>::try_from(chunk).ok()?)))
.flat_map(|chunk| Some(chunk.to_vec()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, we can use StorageWrite::write or StorageWrite::replace. In this case, we don't need to spend 1 byte to store the size of the vector. As we did for ContractsRawCode table.

Thanks to @Voxelot for pointing of this idea=)

.collect();

let unset_count = storage
Expand Down
Loading
Loading