From 8bb83d7016f0f7b4bfabd1a0f7115e567fb345b5 Mon Sep 17 00:00:00 2001 From: Artem Fomiuk <88630083+Artemka374@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:48:18 +0200 Subject: [PATCH] Storage refactoring docs (#1592) * add example and storage refactoring docs * add generic struct with storage key to example * add explanation for storage keys * add more types to example and update README * Make docs more descriptive about changes * Explain concatenation in more details * Link to use.ink where necessary * Apply suggestions --------- Co-authored-by: ivan770 --- .../complex-storage-structures/.gitignore | 9 + .../complex-storage-structures/Cargo.toml | 30 ++ .../complex-storage-structures/README.md | 267 ++++++++++++++++++ .../complex-storage-structures/lib.rs | 98 +++++++ 4 files changed, 404 insertions(+) create mode 100755 integration-tests/complex-storage-structures/.gitignore create mode 100644 integration-tests/complex-storage-structures/Cargo.toml create mode 100644 integration-tests/complex-storage-structures/README.md create mode 100644 integration-tests/complex-storage-structures/lib.rs diff --git a/integration-tests/complex-storage-structures/.gitignore b/integration-tests/complex-storage-structures/.gitignore new file mode 100755 index 00000000000..8de8f877e47 --- /dev/null +++ b/integration-tests/complex-storage-structures/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/integration-tests/complex-storage-structures/Cargo.toml b/integration-tests/complex-storage-structures/Cargo.toml new file mode 100644 index 00000000000..332d1fda5c3 --- /dev/null +++ b/integration-tests/complex-storage-structures/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "complex_storage_structures" +version = "4.1.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +name = "complex_storage_structures" +path = "lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/complex-storage-structures/README.md b/integration-tests/complex-storage-structures/README.md new file mode 100644 index 00000000000..85939aa00c7 --- /dev/null +++ b/integration-tests/complex-storage-structures/README.md @@ -0,0 +1,267 @@ +# Storage refactoring + +In ink! v4 the way storage works was refactored. + +## ink! v4 storage + +First of all, new version of ink!'s storage substantially changes +the way you can interact with "spread structs" (structs that span multiple +storage cells, for which you had to use `SpreadLayout` in previous versions of ink!) +by allocating storage keys in compile-time. + +For example, consider the previous struct with `SpreadLayout` derived: + +```rust +#[derive(SpreadLayout)] +struct TestStruct { + first: Mapping, + second: Mapping +} +``` + +With new ink! version, it looks like this: + +```rust +#[ink::storage_item] +struct TestStruct { + first: Mapping, + second: Mapping +} +``` + +The compiler will automatically allocate storage keys for your fields, +without relying on fields iteration like in the previous ink! version. + +With these changes, `SpreadLayout` trait was removed, and methods like `pull_spread` and `push_spread` are now unavailable. + +A new trait, `Storable`, was introduced instead. It represents types that can be read and written into the contract's storage. Any type that implements `scale::Encode` and `scale::Decode` +automatically implements `Storable`. + +You can also use `#[ink::storage_item]` to automatically implement `Storable` +and make [your struct](https://use.ink/datastructures/custom-datastructure#using-custom-types-on-storage) fully compatible with contract's storage. This attribute +automatically implements all necessary traits and calculates storage keys for types. +You can also set `#[ink::storage_item(derive = false)]` to remove auto-derive +and derive everything manually later: + +```rust +#[ink::storage_item] +struct MyNonPackedStruct { + first_field: u32, + second_field: Mapping, +} + +#[ink::storage_item(derive = false)] +#[derive(Storable, StorableHint, StorageKey)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +struct MyAnotherNonPackedStruct { + first_field: Mapping>, + second_field: Mapping, +} +``` + +For [precise storage key configuration](https://use.ink/datastructures/storage-layout#manual-vs-automatic-key-generation) several new types were introduced: + +* `StorableHint` is a trait that describes the stored type, and its storage key. +* `ManualKey` is a type, that describes the storage key itself. You can, for example, +set it to a custom value - `ManualKey<123>`. +* `AutoKey` is a type, that gets automatically replaced with the `ManualKey` with +compiler-generated storage key. + +For example, if you want to use the `Mapping`, and you want to set the storage key manually, you can take a look at the following example: + +```rust +#[ink::storage_item] +struct MyStruct { + first_field: u32, + second_field: Mapping>, +} +``` + +For [packed structs](https://use.ink/datastructures/storage-layout#packed-vs-non-packed-layout), a new trait was introduced - `Packed`. It represents structs, +all fields of which occupy a single storage cell. Any type that implements +`scale::Encode` and `scale::Decode` receives a `Packed` implementation: + +Unlike non-packed types created with `#[ink::storage_item]`, packed types don't have +their own storage keys. + +```rust +#[derive(scale::Encode, scale::Decode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +struct MyPackedStruct { + first_field: u32, + second_field: Vec, +} +``` + +Example of nested storage types: + +```rust +#[ink::storage_item] +struct NonPacked { + s1: Mapping, + s2: Lazy, +} + +#[derive(scale::Decode, scale::Encode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +struct Packed { + s1: u128, + s2: Vec, +} + +#[ink::storage_item] +struct NonPackedComplex { + s1: (String, u128, Packed), + s2: Mapping, + s3: Lazy, + s4: Mapping, + s5: Lazy, + s6: PackedGeneric, + s7: NonPackedGeneric, +} +``` + +Every non-packed type also has `StorageKey` trait implemented for them. This trait is used for calculating storage key types. + +There also exists way to use `StorageKey` for types that are packed - you can just use `Lazy`, a wrapper around type +which allows to store it in [separate storage cell under it's own storage key](https://use.ink/datastructures/storage-layout#eager-loading-vs-lazy-loading). You can use it like this: + +```rust +#[ink::storage_item] +struct MyStruct { + first_field: Lazy, + second_field: Mapping, +} +``` + +In this case, `first_field` will be stored in it's own storage cell. + +If you add generic that implements `StorageKey` to your type, it will be used as a storage key for this type, otherwise it will be +set to `AutoKey`. For example this struct has its storage key automatically derived by the compiler: + +```rust +#[ink::storage_item] +struct MyStruct { + first_field: u32, + second_field: Mapping, +} +``` + +On the other hand, you can manually set storage key offset for your struct. This offset will apply to every non-packed field in a struct: + +```rust +#[ink::storage_item] +struct MyStruct { + first_field: u32, + second_field: Mapping>, +} +``` + +When your struct has a `KEY` generic existing, the `#[ink::storage_item]` macro will automatically set +the `ParentKey` generic value to `KEY`, basically concatenating two values together. + +The reason to do it in such way is that you can use the same type in different places and set different storage keys for them. + +For example if you want to use it in contract, you can do it like this: + +```rust +#[ink(storage)] +struct MyContract { + my_struct: MyStruct>, +} +``` + +or + +```rust +#[ink(storage)] +struct MyContract { + my_struct: MyStruct, +} +``` + +After that, if you try to assign the new value to a field of this type, you will get an error, because after code generation, +it will be another type with generated storage key: + +```rust +#[ink(constructor)] +pub fn new() -> Self { + let mut instance = Self::default(); + + instance.balances = Balances::>::default(); + + instance +} +``` + +You will get an error that look similar to this: + +```shell +note: expected struct `Balances, ManualKey<4162912002>>>` +found struct `Balances>` +``` + +That's so, because every type is unique and has it's own storage key after code generation. + +So, the way to fix it is to use `Default::default()` so it will generate right type: + +```rust +instance.balances = Default::default(); +``` + +### Caveats + +There is a known problem with generic fields that are non-packed in structs. Example: + +```rust +#[ink::storage_item] +struct MyNonPackedStruct { + first_field: u32, + second_field: D, +} + +struct OtherStruct { + other_first_field: Mapping, + other_second_field: Mapping>, +} + +trait MyTrait { + fn do_something(&self); +} + +impl MyTrait for OtherStruct { + fn do_something(&self) { + // do something + } +} +``` + +In this case contract cannot be built because it cannot calculate the storage key for the field `second_field` of type `MyTrait`. + +You can use packed structs for it or, as a temporary solution, set `ManualKey` as another trait for field: + +```rust +struct MyNonPackedStruct = OtherStruct> +``` + +But instead of a `ManualKey<123>` you should use key that was generated during compilation. Packed generics work okay, so you can use it like this: + +```rust +#[ink::storage_item] +struct MyNonPackedStruct { + first_field: u32, + second_field: D, +} +``` + +You should also check the [ink! storage layout documentation](https://use.ink/datastructures/storage-layout#considerations) for more +details on known caveats and considerations. diff --git a/integration-tests/complex-storage-structures/lib.rs b/integration-tests/complex-storage-structures/lib.rs new file mode 100644 index 00000000000..6adbfbbe650 --- /dev/null +++ b/integration-tests/complex-storage-structures/lib.rs @@ -0,0 +1,98 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[ink::contract] +pub mod complex_structures { + use ink::storage::{ + traits::{AutoKey, ManualKey, Storable, StorableHint, StorageKey}, + Mapping, + }; + + /// Non-packed type usage + #[ink::storage_item(derive = false)] + #[derive(Storable, StorableHint, StorageKey, Default, Debug)] + #[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) + )] + pub struct TokenManagement { + balances: Balances, + allowances: Allowances>, + } + + #[ink::storage_item] + #[derive(Default, Debug)] + pub struct Allowances { + allowances: Mapping<(AccountId, AccountId), Balance, AutoKey>, + } + + #[derive(scale::Encode, scale::Decode, Default, Debug)] + #[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) + )] + pub struct Balances { + pub balance_state: u128, + } + + impl Allowances { + fn get_allowance(&self, owner: AccountId, spender: AccountId) -> Balance { + self.allowances.get(&(owner, spender)).unwrap_or(0) + } + + fn set_allowance( + &mut self, + owner: AccountId, + spender: AccountId, + value: Balance, + ) { + self.allowances.insert(&(owner, spender), &value); + } + } + + #[ink(storage)] + #[derive(Default)] + pub struct Contract { + pub token_management: TokenManagement, + } + + impl Contract { + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + #[ink(message)] + pub fn increase_balances_state(&mut self, amount: u128) { + self.token_management.balances.balance_state += amount; + } + + #[ink(message)] + pub fn decrease_balances_state(&mut self, amount: u128) { + self.token_management.balances.balance_state -= amount; + } + + #[ink(message)] + pub fn get_balances_state(&self) -> u128 { + self.token_management.balances.balance_state + } + + #[ink(message)] + pub fn get_allowance(&self, owner: AccountId, spender: AccountId) -> u128 { + self.token_management + .allowances + .get_allowance(owner, spender) + } + + #[ink(message)] + pub fn set_allowance( + &mut self, + owner: AccountId, + spender: AccountId, + value: u128, + ) { + self.token_management + .allowances + .set_allowance(owner, spender, value) + } + } +}