Skip to content

Commit 8bb83d7

Browse files
Artemka374ivan770
andauthored
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 <[email protected]>
1 parent c2e0857 commit 8bb83d7

File tree

4 files changed

+404
-0
lines changed

4 files changed

+404
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Ignore build artifacts from the local tests sub-crate.
2+
/target/
3+
4+
# Ignore backup files creates by cargo fmt.
5+
**/*.rs.bk
6+
7+
# Remove Cargo.lock when creating an executable, leave it for libraries
8+
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
9+
Cargo.lock
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "complex_storage_structures"
3+
version = "4.1.0"
4+
authors = ["Parity Technologies <[email protected]>"]
5+
edition = "2021"
6+
publish = false
7+
8+
[dependencies]
9+
ink = { path = "../../crates/ink", default-features = false }
10+
11+
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
12+
scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true }
13+
14+
[dev-dependencies]
15+
ink_e2e = { path = "../../crates/e2e" }
16+
17+
[lib]
18+
name = "complex_storage_structures"
19+
path = "lib.rs"
20+
crate-type = ["cdylib"]
21+
22+
[features]
23+
default = ["std"]
24+
std = [
25+
"ink/std",
26+
"scale/std",
27+
"scale-info/std",
28+
]
29+
ink-as-dependency = []
30+
e2e-tests = []
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Storage refactoring
2+
3+
In ink! v4 the way storage works was refactored.
4+
5+
## ink! v4 storage
6+
7+
First of all, new version of ink!'s storage substantially changes
8+
the way you can interact with "spread structs" (structs that span multiple
9+
storage cells, for which you had to use `SpreadLayout` in previous versions of ink!)
10+
by allocating storage keys in compile-time.
11+
12+
For example, consider the previous struct with `SpreadLayout` derived:
13+
14+
```rust
15+
#[derive(SpreadLayout)]
16+
struct TestStruct {
17+
first: Mapping<u32, u32>,
18+
second: Mapping<u64, u64>
19+
}
20+
```
21+
22+
With new ink! version, it looks like this:
23+
24+
```rust
25+
#[ink::storage_item]
26+
struct TestStruct {
27+
first: Mapping<u32, u32>,
28+
second: Mapping<u64, u64>
29+
}
30+
```
31+
32+
The compiler will automatically allocate storage keys for your fields,
33+
without relying on fields iteration like in the previous ink! version.
34+
35+
With these changes, `SpreadLayout` trait was removed, and methods like `pull_spread` and `push_spread` are now unavailable.
36+
37+
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`
38+
automatically implements `Storable`.
39+
40+
You can also use `#[ink::storage_item]` to automatically implement `Storable`
41+
and make [your struct](https://use.ink/datastructures/custom-datastructure#using-custom-types-on-storage) fully compatible with contract's storage. This attribute
42+
automatically implements all necessary traits and calculates storage keys for types.
43+
You can also set `#[ink::storage_item(derive = false)]` to remove auto-derive
44+
and derive everything manually later:
45+
46+
```rust
47+
#[ink::storage_item]
48+
struct MyNonPackedStruct {
49+
first_field: u32,
50+
second_field: Mapping<u32, u32>,
51+
}
52+
53+
#[ink::storage_item(derive = false)]
54+
#[derive(Storable, StorableHint, StorageKey)]
55+
#[cfg_attr(
56+
feature = "std",
57+
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
58+
)]
59+
struct MyAnotherNonPackedStruct {
60+
first_field: Mapping<u128, Vec<u8>>,
61+
second_field: Mapping<u32, u32>,
62+
}
63+
```
64+
65+
For [precise storage key configuration](https://use.ink/datastructures/storage-layout#manual-vs-automatic-key-generation) several new types were introduced:
66+
67+
* `StorableHint` is a trait that describes the stored type, and its storage key.
68+
* `ManualKey` is a type, that describes the storage key itself. You can, for example,
69+
set it to a custom value - `ManualKey<123>`.
70+
* `AutoKey` is a type, that gets automatically replaced with the `ManualKey` with
71+
compiler-generated storage key.
72+
73+
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:
74+
75+
```rust
76+
#[ink::storage_item]
77+
struct MyStruct {
78+
first_field: u32,
79+
second_field: Mapping<u32, u32, ManualKey<123>>,
80+
}
81+
```
82+
83+
For [packed structs](https://use.ink/datastructures/storage-layout#packed-vs-non-packed-layout), a new trait was introduced - `Packed`. It represents structs,
84+
all fields of which occupy a single storage cell. Any type that implements
85+
`scale::Encode` and `scale::Decode` receives a `Packed` implementation:
86+
87+
Unlike non-packed types created with `#[ink::storage_item]`, packed types don't have
88+
their own storage keys.
89+
90+
```rust
91+
#[derive(scale::Encode, scale::Decode)]
92+
#[cfg_attr(
93+
feature = "std",
94+
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
95+
)]
96+
struct MyPackedStruct {
97+
first_field: u32,
98+
second_field: Vec<u8>,
99+
}
100+
```
101+
102+
Example of nested storage types:
103+
104+
```rust
105+
#[ink::storage_item]
106+
struct NonPacked {
107+
s1: Mapping<u32, u128>,
108+
s2: Lazy<u128>,
109+
}
110+
111+
#[derive(scale::Decode, scale::Encode)]
112+
#[cfg_attr(
113+
feature = "std",
114+
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
115+
)]
116+
struct Packed {
117+
s1: u128,
118+
s2: Vec<u128>,
119+
}
120+
121+
#[ink::storage_item]
122+
struct NonPackedComplex<KEY: StorageKey> {
123+
s1: (String, u128, Packed),
124+
s2: Mapping<u128, u128>,
125+
s3: Lazy<u128>,
126+
s4: Mapping<u128, Packed>,
127+
s5: Lazy<NonPacked>,
128+
s6: PackedGeneric<Packed>,
129+
s7: NonPackedGeneric<Packed>,
130+
}
131+
```
132+
133+
Every non-packed type also has `StorageKey` trait implemented for them. This trait is used for calculating storage key types.
134+
135+
There also exists way to use `StorageKey` for types that are packed - you can just use `Lazy`, a wrapper around type
136+
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:
137+
138+
```rust
139+
#[ink::storage_item]
140+
struct MyStruct {
141+
first_field: Lazy<u32>,
142+
second_field: Mapping<u32, u32>,
143+
}
144+
```
145+
146+
In this case, `first_field` will be stored in it's own storage cell.
147+
148+
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
149+
set to `AutoKey`. For example this struct has its storage key automatically derived by the compiler:
150+
151+
```rust
152+
#[ink::storage_item]
153+
struct MyStruct {
154+
first_field: u32,
155+
second_field: Mapping<u32, u32>,
156+
}
157+
```
158+
159+
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:
160+
161+
```rust
162+
#[ink::storage_item]
163+
struct MyStruct<KEY: StorageKey> {
164+
first_field: u32,
165+
second_field: Mapping<u32, u32, ManualKey<123>>,
166+
}
167+
```
168+
169+
When your struct has a `KEY` generic existing, the `#[ink::storage_item]` macro will automatically set
170+
the `ParentKey` generic value to `KEY`, basically concatenating two values together.
171+
172+
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.
173+
174+
For example if you want to use it in contract, you can do it like this:
175+
176+
```rust
177+
#[ink(storage)]
178+
struct MyContract {
179+
my_struct: MyStruct<ManualKey<123>>,
180+
}
181+
```
182+
183+
or
184+
185+
```rust
186+
#[ink(storage)]
187+
struct MyContract {
188+
my_struct: MyStruct<AutoKey>,
189+
}
190+
```
191+
192+
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,
193+
it will be another type with generated storage key:
194+
195+
```rust
196+
#[ink(constructor)]
197+
pub fn new() -> Self {
198+
let mut instance = Self::default();
199+
200+
instance.balances = Balances::<ManualKey<123>>::default();
201+
202+
instance
203+
}
204+
```
205+
206+
You will get an error that look similar to this:
207+
208+
```shell
209+
note: expected struct `Balances<ResolverKey<ManualKey<_, _>, ManualKey<4162912002>>>`
210+
found struct `Balances<ManualKey<_, _>>`
211+
```
212+
213+
That's so, because every type is unique and has it's own storage key after code generation.
214+
215+
So, the way to fix it is to use `Default::default()` so it will generate right type:
216+
217+
```rust
218+
instance.balances = Default::default();
219+
```
220+
221+
### Caveats
222+
223+
There is a known problem with generic fields that are non-packed in structs. Example:
224+
225+
```rust
226+
#[ink::storage_item]
227+
struct MyNonPackedStruct<D: MyTrait = OtherStruct> {
228+
first_field: u32,
229+
second_field: D,
230+
}
231+
232+
struct OtherStruct {
233+
other_first_field: Mapping<u128, u128>,
234+
other_second_field: Mapping<u32, Vec<u8>>,
235+
}
236+
237+
trait MyTrait {
238+
fn do_something(&self);
239+
}
240+
241+
impl MyTrait for OtherStruct {
242+
fn do_something(&self) {
243+
// do something
244+
}
245+
}
246+
```
247+
248+
In this case contract cannot be built because it cannot calculate the storage key for the field `second_field` of type `MyTrait`.
249+
250+
You can use packed structs for it or, as a temporary solution, set `ManualKey` as another trait for field:
251+
252+
```rust
253+
struct MyNonPackedStruct<D: MyTrait + ManualKey<123> = OtherStruct>
254+
```
255+
256+
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:
257+
258+
```rust
259+
#[ink::storage_item]
260+
struct MyNonPackedStruct<D: Packed> {
261+
first_field: u32,
262+
second_field: D,
263+
}
264+
```
265+
266+
You should also check the [ink! storage layout documentation](https://use.ink/datastructures/storage-layout#considerations) for more
267+
details on known caveats and considerations.

0 commit comments

Comments
 (0)