diff --git a/static/img/kv.svg b/static/img/kv.svg index d9ce02507b..22a136c609 100644 --- a/static/img/kv.svg +++ b/static/img/kv.svg @@ -1,7 +1,56 @@ - - - - diff --git a/versioned_docs/version-4.0.0-alpha.1/datastructures/custom.md b/versioned_docs/version-4.0.0-alpha.1/datastructures/custom.md index 3b7eec23f7..39943dcbf8 100644 --- a/versioned_docs/version-4.0.0-alpha.1/datastructures/custom.md +++ b/versioned_docs/version-4.0.0-alpha.1/datastructures/custom.md @@ -3,48 +3,139 @@ title: Custom Data Structures slug: /datastructures/custom-datastructure --- -:::caution -TODO +The `ink_storage` crate provides useful utilities and data structures to organize and +manipulate the contract's storage. However, contract authors should know that they can +also create their own custom data structures. -Beware, this page is no longer up to date for 4.0! -::: +## Using custom types on storage +Any custom type wanting to be compatible with ink! storage must implement the +[`Storable`](https://docs.rs/ink_storage_traits/4.0.0-beta/ink_storage_traits/trait.Storable.html) +trait, so it can be SCALE +[`encoded`](https://docs.rs/parity-scale-codec/3.2.2/parity_scale_codec/trait.Encode.html) +and +[`decoded`](https://docs.rs/parity-scale-codec/3.2.2/parity_scale_codec/trait.Decode.html). +Additionally, the traits +[`StorageLayout`](https://docs.rs/ink_storage/latest/ink_storage/traits/trait.StorageLayout.html) +and [`TypeInfo`](https://docs.rs/scale-info/2.3.1/scale_info/trait.TypeInfo.html) +are required as well. But don't worry, usually these traits can just be derived: + +```rust +/// A custom type that we can use in our contract storage +#[derive(scale::Decode, scale::Encode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct Inner { + value: bool, +} -While the `ink_storage` crate provides tons of useful utilities and data structures to organize and manipulate the contract's storage contract authors are not limited by its capabilities. By implementing the core `SpreadLayout`/`PackedLayout` traits (and the `StorageLayout` trait for supporting the metadata generated for the `.contract` bundle) users are able to define their very own custom storage data structures with their own set of requirement and features that work along the `ink_storage` data structures as long as they fulfill the mere requirements stated by those two traits. +#[ink(storage)] +pub struct ContractStorage { + inner: Inner, +} +``` -A basic example of a custom struct is shown below: +Even better: there is a macro +[`#[ink::storage_item`](https://docs.rs/ink_macro/4.0.0-beta.1/ink_macro/attr.storage_item.html), +which derives all necessary traits for you. If there is no need to implement any special +behaviour, the above code example can be simplified further as follows: -``` rust -struct Inner { - value: bool +```rust +/// A custom type that we can use in our contract storage +#[ink::storage_item] +pub struct Inner { + value: bool, } #[ink(storage)] -pub struct MyContract { - inner: Inner +pub struct ContractStorage { + inner: Inner, } ``` -Compiling the above will result in errors. While having an inner struct which holds only a boolean might not be the best idea, it serves well to illustrate how to implement the trait: +Naturally, you can as well implement any required trait manually. Please directly refer to +the relevant trait documentations for more information. + +:::note + +The `#[ink::storage_item]` macro is responsible for storage key calculation of +non-[`Packed`](https://docs.rs/ink_storage_traits/4.0.0-beta.1/ink_storage_traits/trait.Packed.html) +types. Without it, the key for non-`Packed` fields will be zero. Using this macro is +necessary if you don't plan to use a +[`ManualKey`](https://docs.rs/ink_storage_traits/4.0.0-beta.1/ink_storage_traits/struct.ManualKey.html) +on a non-`Packed` type. + +Types with custom implementations of the ink! storage traits can still use this macro only +for key calculation by disabling the derives: `#[ink::storage_item(derive = false)]`. + +::: + +## Generic storage fields + +It is possible to use generic data types in your storage, as long as any generic type +satisfies the required storage trait bounds. In fact, we already witnessed this in the +previous sections about the +[`Mapping`](https://docs.rs/ink_storage/4.0.0-beta.1/ink_storage/struct.Mapping.html). -``` rust -impl SpreadLayout for Inner { - const FOOTPRINT: u64 = 1; +Let's say you want a mapping where accessing a non-existent key should just return +it's default value, akin to how mappings work in Solidity. Additionally, you want to know +how many values there are in the mapping (its length). This could be implemented as a +thin wrapper around the ink! `Mapping` as follows: - fn pull_spread(ptr: &mut KeyPtr) -> Self { - Self { - value: SpreadLayout::pull_spread(ptr), +```rust +/// Values for this map need to implement the `Default` trait. +/// Naturally, they also must be compatible with contract storage. +/// Note that the underlying `Mapping` type only supports `Packed` values. +#[ink::storage_item] +pub struct DefaultMap { + values: Mapping, + length: u32, +} + +impl DefaultMap { + /// Accessing non-existent keys will return the default value. + pub fn get(&self, key: &K) -> V { + self.values.get(key).unwrap_or_default() + } + + /// Inserting into the map increases its length by one. + pub fn set(&mut self, key: I, value: &U) + where + I: scale::EncodeLike, + E: scale::EncodeLike + Storable, + { + if self.values.insert(key, value).is_none() { + self.length += 1 } } - fn push_spread(&self, ptr: &mut KeyPtr) { - SpreadLayout::push_spread(&self.value, ptr); + /// Removing a value from the map decreases its length by one. + pub fn remove(&mut self, key: &K) { + if self.values.take(key).is_some() { + self.length -= 1 + } } - fn clear_spread(&self, ptr: &mut KeyPtr) { - SpreadLayout::clear_spread(&self.value, ptr); + /// Return how many values the mapping contains + pub fn len(&self) -> u32 { + self.length } } +/// `DefaultMap` is compatible with contract storage. +#[ink(storage)] +pub struct MyContract { + my_map: DefaultMap, +} ``` -You can check what each method does in the [trait's docs](https://docs.rs/ink_storage/4.0.0-beta/ink_storage/traits/trait.SpreadLayout.html). Check how some data structures are implemented, such as [Mapping](https://docs.rs/ink_storage/4.0.0-beta/src/ink_storage/lazy/mapping.rs.html#113). +:::caution + +Generic data types may substantially increase your contracts overall code size, making it +more costly to store on-chain. + +The reason for this is [Rust's monomorphization](https://rustwasm.github.io/twiggy/concepts/generic-functions-and-monomorphization.html). + +::: + diff --git a/versioned_docs/version-4.0.0-alpha.1/datastructures/mapping.md b/versioned_docs/version-4.0.0-alpha.1/datastructures/mapping.md index 4b759dd94d..ceaf4043bf 100644 --- a/versioned_docs/version-4.0.0-alpha.1/datastructures/mapping.md +++ b/versioned_docs/version-4.0.0-alpha.1/datastructures/mapping.md @@ -1,78 +1,131 @@ --- -title: Working with Mapping +title: Working with Mapping slug: /datastructures/mapping --- -:::caution -TODO +In this section we demonstrate how to work with ink! [`Mapping`](https://docs.rs/ink_storage/4.0.0-beta.1/ink_storage/struct.Mapping.html). -Beware, this page is no longer up to date for 4.0! -::: - -In this section we want to demonstrate how to work with ink! [`Mapping`](https://docs.rs/ink_storage/4.0.0-beta/ink_storage/struct.Mapping.html). - -Here is an example of a mapping from a user to a number: +Here is an example of a mapping from a user to a `Balance`: ```rust #[ink(storage)] -#[derive(SpreadAllocate)] pub struct MyContract { - // Store a mapping from AccountIds to a u32 - map: ink_storage::Mapping, + /// Assign a balance to every account. + balances: ink::storage::Mapping, } ``` This means that for a given key, you can store a unique instance of a value type. In this -case, each "user" gets their own number. - -## Initializing a Mapping +case, each "user" gets credited their own balance. -In order to correctly initialize a `Mapping` we need two things: -1. An implementation of the [`SpreadAllocate`](https://docs.rs/ink_storage/4.0.0-beta/ink_storage/traits/trait.SpreadAllocate.html) trait on our storage struct -2. The [`ink_lang::utils::initalize_contract`](https://docs.rs/ink_lang/4.0.0-beta/ink_lang/utils/fn.initialize_contract.html) initializer +## Example: Using a `Mapping` -Not initializing storage before you use it is a common mistake that can break your smart -contract. If you do not initialize your `Mapping`'s correctly you may end up with -different `Mapping`'s operating on the same set of storage entries 😱. +The following example contract utilizes a `Mapping` so that anyone can deposit and withdraw +balance for their own account: ```rust #![cfg_attr(not(feature = "std"), no_std)] #[ink::contract] mod mycontract { - use ink_storage::traits::SpreadAllocate; + use ink::storage::Mapping; #[ink(storage)] - #[derive(SpreadAllocate)] pub struct MyContract { - // Store a mapping from AccountIds to a u32 - map: ink_storage::Mapping, + /// Assign a balance to every account ID + balances: Mapping, } impl MyContract { - #[ink(constructor)] - pub fn new(count: u32) -> Self { - // This call is required in order to correctly initialize the - // `Mapping`s of our contract. - ink_lang::utils::initialize_contract(|contract: &mut Self| { - let caller = Self::env().caller(); - contract.map.insert(&caller, &count); - }) + /// Constructor to initialize the contract with an empty mapping. + #[ink(constructor, payable)] + pub fn new() -> Self { + let balances = Mapping::default(); + Self { balances } } - #[ink(constructor)] - pub fn default() -> Self { - // Even though we're not explicitly initializing the `Mapping`, - // we still need to call this - ink_lang::utils::initialize_contract(|_| {}) + /// Retrieve the balance of the caller. + #[ink(message)] + pub fn get_balance(&self) -> Option { + let caller = self.env().caller(); + self.balances.get(caller) } - // Grab the number at the caller's AccountID, if it exists - #[ink(message)] - pub fn get(&self) -> u32 { - let caller = Self::env().caller(); - self.map.get(&caller).unwrap_or_default() + /// Credit more money to the contract. + #[ink(message, payable)] + pub fn transfer(&mut self) { + let caller = self.env().caller(); + let balance = self.balances.get(caller).unwrap_or(0); + let endowment = self.env().transferred_value(); + self.balances.insert(caller, &(balance + endowment)); + } + + /// Withdraw all your balance from the contract. + pub fn withdraw(&mut self) { + let caller = self.env().caller(); + let balance = self.balances.get(caller).unwrap(); + self.balances.remove(caller); + self.env().transfer(caller, balance).unwrap() } } } + +``` + +## Considerations when using the `Mapping` type + +One of the main purposes of the ink! `Mapping` is to allow storing a lot of values. + +:::note + +There are many additional datastructures accessible under `ink::prelude::collections`, such +such as `HashMap` or `BTreeMap` (to name a few). Note that these datastructures all exhibit +`Packed` storage loading behavior, as opposed to the ink! `Mapping`! + +::: + +### Storage loading behaviour + +Each `Mapping` value lives under it's own storage key. Briefly, this means that `Mapping`s +are lazily loaded in ink!. In other words, if your message only accesses a single key of a +mapping, it will not load the whole mapping but only the value being accessed. + +```rust +// This causes only a single storage access and the decoding of a single "MyValue" struct, +// no matter how many elements there are inside the mapping. +let foo: MyValue = my_mapping.get(0)?; + +for n in 0..5 { + // This causes a storage access and a decoding operation for each loop iteration. + // It is not possible to "fetch" all key/value pairs directly at once. + let bar: MyValue = my_mapping.get(n)?; +} +``` + +Furthermore, it follows that mapping values do not have a contiguous storage layout and it is +not possible to iterate over the contents of a map. + + +### Updating values + +The attentive reader may have noticed that accessing mapping values via the `Mapping::get()` +method will result in an owned value (a local copy), as opposed to a direct reference +into the storage. Changes to this value won't be reflected in the contracts storage +"automatically". To avoid this common pitfall, the value must be inserted again at the same +key after it was modified. The `transfer` function from above example illustrates this: + +```rust +pub fn transfer(&mut self) { + let caller = self.env().caller(); + // `balance` is a local value and not a reference to the value on storage! + let balance = self.balances.get(caller).unwrap_or(0); + let endowment = self.env().transferred_value(); + // The following line of code would have no effect to the balance of the + // caller stored in contract storage: + // + // balance += endowment; + // + // Instead, we use the `insert` function to write it back like so: + self.balances.insert(caller, &(balance + endowment)); +} ``` diff --git a/versioned_docs/version-4.0.0-alpha.1/datastructures/overview.md b/versioned_docs/version-4.0.0-alpha.1/datastructures/overview.md index 42fda8ace9..f92c4154a1 100644 --- a/versioned_docs/version-4.0.0-alpha.1/datastructures/overview.md +++ b/versioned_docs/version-4.0.0-alpha.1/datastructures/overview.md @@ -3,46 +3,25 @@ title: Overview slug: /datastructures/overview --- -The `ink_storage` crate acts as the standard storage library for ink! smart contracts. At -the moment it only provides a single low-level primitive for interacting with storage, -the [`Mapping`](https://docs.rs/ink_storage/4.0.0-beta/ink_storage/struct.Mapping.html). - -The `Mapping` is a mapping of key-value pairs directly to the contract storage. Its main advantage -is to be simple and lightweight. As such, it does not provide any high-level -functionality, such as iteration or automatic clean-up. Smart contract authors will need -to implement any high level functionality themselves. - -## Eager Loading - -When executing a contract, all the fields of the `#[ink(storage)]` struct will be pulled -from storage, regardless of whether or not they are used during the message execution. - -Smart contract authors should be aware of this behaviour since it could potentially -affect their contract performance. For example, consider the following storage struct: - -```rust -#[ink(storage)] -pub struct EagerLoading { - a: i32, - b: ink_prelude::vec::Vec, -} - -impl EagerLoading { - #[ink(message)] - pub fn read_a(&self) { - let a = self.a; - } -} -``` - -In `EagerLoading::read_a()` we only read the `a` storage item. However, the `b` storage -item will still be loaded from storage. As a reminder, this means accessing the -underlying database and SCALE decoding the value. This can incur high costs, especially -as the number of elements in `b` grows. - -:::note - -Eager loading does **not** apply to `Mapping` fields, though, as key lookups in mappings -are done directly from contract storage. - -::: +The `ink_storage` crate acts as the standard storage library for ink! smart contracts. +At the moment it provides two primitives for interacting with storage, +[`Mapping`](https://docs.rs/ink_storage/4.0.0-beta.1/ink_storage/struct.Mapping.html) +and [`Lazy`](https://docs.rs/ink_storage/4.0.0-beta.1/ink_storage/struct.Lazy.html). + +`Mapping` is a mapping of key-value pairs directly to the contract storage. It is very +similar to traditional hash tables and comparable to the `mapping` type Solidity offers. +As a core ingredient to the ink! language, its main advantage is being simple and +lightweight: It favors being efficient in terms of gas costs and code size +over providing a lot of high-level functionality found in other implementations +like the `ink::prelude::collections::HashMap` type. +Overall, the ink! `Mapping` will be solid choice for most contracts. Moreover, smart +contracts developers can implement advanced features themselves. + +`Lazy` is a wrapper type that can be used over any other storage compatible type. +This allows smart contract developers fine grained manual control over the layout of +the contract storage by assigning a separate storage cell for the field. For example, +it can be used to prevent the contract from eagerly loading large storage fields +during each contract call. +Conceivably, it may be desirable to change certain aspects on how your contract deals with +its storage variables. You can find out more about this in the section about the ink! +[Storage Layout](https://use.ink/versioned_docs/version-4.0.0-alpha.1/datastructures/storage-layout). diff --git a/versioned_docs/version-4.0.0-alpha.1/datastructures/spread-storage-layout.md b/versioned_docs/version-4.0.0-alpha.1/datastructures/spread-storage-layout.md deleted file mode 100644 index 0bdadb7d3f..0000000000 --- a/versioned_docs/version-4.0.0-alpha.1/datastructures/spread-storage-layout.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Spread Storage Layout -slug: /datastructures/spread-storage-layout ---- - -:::caution -TODO - -Beware, this page is no longer up to date for 4.0! -::: - -### Storage Organization - -The following schema depicts the storage which is exposed -to ink! by the contracts pallet: - -
- Storage Organization: Layout -
- -ink!'s storage operates by storing and loading entries into and from a single storage -cell. At the moment there is no way to customize this behaviour. Depending on the data -we're dealing with, this can end up being good or bad. - -For example, if we have a somewhat small `ink_prelude::vec::Vec` loading all the elements -at the same time can be advantegous - especially if we expect our message to interact -with most of them in a single call. - -On the other hand, this can be problematic if we're loading a large `Vec` and only -operating on a few elements. - -### Spreading - -ink! spreads information to as many cells as possible. For example if you have the -following `#[ink(storage)]` struct every field will live in its own single storage cell. -Note that for `b` all 32 bytes will share the same cell! - -```rust -#[ink(storage)] -pub struct Spread { - a: i32, - b: [u8; 32], -} -``` - -The following schema depicts the storage layout for a vector with three elements, -persisted to storage in a spread layout. - -
- Storage Organization: Spreading -
diff --git a/versioned_docs/version-4.0.0-alpha.1/datastructures/storage-in-metadata.md b/versioned_docs/version-4.0.0-alpha.1/datastructures/storage-in-metadata.md index e48ff955f9..c15582d4c1 100644 --- a/versioned_docs/version-4.0.0-alpha.1/datastructures/storage-in-metadata.md +++ b/versioned_docs/version-4.0.0-alpha.1/datastructures/storage-in-metadata.md @@ -3,6 +3,211 @@ title: Metadata Format slug: /datastructures/storage-in-metadata --- -:::caution -TODO: Add documentation on the `storage ` field in the metadata. -::: +The storage layout of a contract is reflected inside the metadata. It allows third-party +tooling to work with contract storage and can also help to better understand the storage +layout of any given contract. + +Given a contract with the following storage: + +```rust +#[ink(storage)] +pub struct MyContract { + balance: Balance, + block: BlockNumber, + lazy: Lazy, +} +``` + +The storage will be reflected inside the metadata as like follows: + +```json +"root": { + "layout": { + "struct": { + "fields": [ + { + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 0 + } + }, + "name": "balance" + }, + { + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 1 + } + }, + "name": "block" + }, + { + "layout": { + "root": { + "layout": { + "leaf": { + "key": "0xb1f4904e", + "ty": 2 + } + }, + "root_key": "0xb1f4904e" + } + }, + "name": "lazy" + } + ], + "name": "MyContract" + } + }, + "root_key": "0x00000000" +} +``` + +We observe that the storage layout is represented as a tree, where tangible storage values +end up inside a `leaf`. Because of +[`Packed`](https://docs.rs/ink_storage_traits/4.0.0-beta.1/ink_storage_traits/trait.Packed.html) +encoding, leafs can share the same storage key, and +in order to reach them you'd need fetch and decode the whole storage cell under this key. + +A `root_key` is meant to either be used to directly access a `Packed` storage field +or to serve as the base key for calculating the actual keys needed to access values in +non-`Packed` fields (such as `Mapping`s). + +## Storage key calculation for ink! `Mapping` values + +Base storage keys are always 4 bytes in size. However, the storage API of the contracts +pallet supports keys of arbitrary length. In order to reach a mapping value, the storage +key of said value is calculated at runtime. + +The formula to calculate the base storage key `S` used to access a mapping value under the +key `K` for a mapping with base key `B` can be expressed as follows: + +``` +S = B + scale::encode(K) +``` + +In words, find the base (root) key of the mapping and concatenate it with the +SCALE encoded key of the mapped value to obtain the actual storage key used to +access the mapped value. + +## Accessing storage items with the `contractsApi` runtime call API + +There are two ways to query for storage fields of smart contracts from outside a contract. +Both methods are accessible via the [`polkadot-js`](https://polkadot.js.org/apps/) web UI. + +The straight forward way to query a contracts storage is via a +[`runtime API`](https://polkadot.js.org/apps/#/runtime) call, using the `contractsApi` +endpoint provided by the contracts pallet. The endpoint provides a `getStorage` method, +which just expects a contract address and a storage key as arguments. + +For example, to access the root storage struct under the key `0x00000000` of a contract, +just specify the contract's address and the storage key `0x00000000` as-is. The API call +will return the scale-encoded root storage struct of the contract. + +## Accessing storage items with the `childState` RPC call API + +Under the hood, each contract gets its own +[child trie](https://paritytech.github.io/substrate/master/frame_support/storage/child/index.html), where its storage items are actually stored. + +Additionally, the contracts pallet uses the +[`Blake2 128 Concat`](https://paritytech.github.io/substrate/master/frame_support/struct.Blake2_128Concat.html) +[`Transparent hashing algorithm`](https://docs.substrate.io/build/runtime-storage/#transparent-hashing-algorithms) +to calculate storage keys for any stored item inside the child trie. +You'll need to account for that as well. + +With that in mind, to directly access storage items of any on-chain contract using a +childState [`RPC call`](https://polkadot.js.org/apps/#/rpc), you'll need the following: +- The child trie ID of the contract, represented as a [`PrefixedStorageKey`](https://docs.rs/sp-storage/10.0.0/sp_storage/struct.PrefixedStorageKey.html) +- The hashed storage key of the storage field + +### Finding the contracts child trie ID + +The child trie ID is the `Blake2_256` hash of the contracts instantiation nonce +concatenated to it's `AccountId`. You can find it in +[`polkadot-js chainstate query interface`](https://polkadot.js.org/apps/#/chainstate), +where you need to execute the `contracts_contractInfoOf` state query. + +It can also be calculate manually according to the following code snippet. The +instantiation note of the contract must be still be known. You can get it using the +`contracts_nonce` chain state query in polkadot-js UI. + +```rust +use sp_core::crypto::Ss58Codec; +use parity_scale_codec::Encode; + +// Given our contract ID is 5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4 +let account: AccountId32 = + Ss58Codec::from_string("5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4").unwrap(); +// Given our instantiation nonce was 1 +let nonce: u64 = 1; + +// The child trie ID can be calculated as follows: +let trie_id = (&account, nonce).using_encoded(Blake2_256::hash); +``` + +### Calculate the `PrefixedStorageKey` from the child trie ID +A `PrefixedStorageKey` based on the child trie ID can be constructed using the `ChildInfo` +primitive as follows: + +```rust +use sp_core::storage::ChildInfo; +let prefixed_storage_key = ChildInfo::new_default(&trie_id).into_prefixed_storage_key(); +``` + +### Calculate the storage key using transparent hashing + +Finally, we calculate the hashed storage key of the storage item we are wanting to access. +The algorithm is simple: `Blake2_128` hash the storage key and then concatenate the unhashed +key to the hash. Given you want to access the storage item under the `0x00000000`, +it will look like this in code: + +```rust +use frame_support::Blake2_128Concat; + +// The base key is 0x00000000 +let storage_key = Blake2_128Concat::hash(&[0, 0, 0, 0]); +``` + +### A full example + +Let's recap the last few paragraphs into a full example. Given: + +* A contract at address `5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4` +* With instantiation nonce of `1` +* The root storage struct is to be found at base key `0x00000000` + +The following Rust program demonstrates how to calculate the `PrefixedStorageKey` of the +contracts child trie, as well as the hashed key for the storage struct, which can then be +used with the `chilstate` RPC endpoint function `getStorage` in polkadot-js to receive +the root storage struct of the contract: + +```rust +use frame_support::{sp_runtime::AccountId32, Blake2_128Concat, Blake2_256, StorageHasher}; +use parity_scale_codec::Encode; +use sp_core::{crypto::Ss58Codec, storage::ChildInfo}; +use std::ops::Deref; + +fn main() { + // Find the child storage trie ID + let account_id = "5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4"; + let account: AccountId32 = Ss58Codec::from_string(account_id).unwrap(); + let instantiation_nonce = 1u64; + let trie_id = (account, instantiation_nonce).using_encoded(Blake2_256::hash); + assert_eq!( + hex::encode(trie_id), + "2fa252b7f996d28fd5d8b11098c09e56295eaf564299c6974421aa5ed887803b" + ); + + // Calculate the PrefixedStorageKey based on the trie ID + let prefixed_storage_key = ChildInfo::new_default(&trie_id).into_prefixed_storage_key(); + println!("0x{}", hex::encode(prefixed_storage_key.deref())); + // 0x3a6368696c645f73746f726167653a64656661756c743a2fa252b7f996d28fd5d8b11098c09e56295eaf564299c6974421aa5ed887803b + + // Calculate the storage key using transparent hashing + let storage_key = Blake2_128Concat::hash(&[0, 0, 0, 0]); + println!("0x{}", hex::encode(&storage_key)); + // 0x11d2df4e979aa105cf552e9544ebd2b500000000 +} +``` diff --git a/versioned_docs/version-4.0.0-alpha.1/datastructures/storage-layout.md b/versioned_docs/version-4.0.0-alpha.1/datastructures/storage-layout.md new file mode 100644 index 0000000000..79733371bc --- /dev/null +++ b/versioned_docs/version-4.0.0-alpha.1/datastructures/storage-layout.md @@ -0,0 +1,159 @@ +--- +title: Storage Layout +slug: /datastructures/storage-layout +--- + +Smart contract authors are given some flexibility in regards on how they want to organize +the storage layout of their contracts. +Let's dive deeper into the concepts behind ink! storage to get a better understanding +of some of its implications and limitations. + +## Storage Organization + +The following schema depicts the storage which is exposed +to ink! by the contracts pallet: + +
+ Storage Organization: Layout +
+ +Storage data is always encoded with the +[`SCALE`](https://docs.substrate.io/reference/scale-codec/) codec. +The storage API operates by storing and loading entries into and from a single storages +cells, where each storage cell is accessed under its own dedicated storage key. To some +extent, the storage API works similar to a traditional key-value database. + +## Packed vs Non-Packed layout + +Types that can be stored entirely under a single storage cell are considered +[`Packed`](https://docs.rs/ink_storage_traits/4.0.0-beta.1/ink_storage_traits/trait.Packed.html). +By default, ink! tries to store all storage struct fields under a single storage cell. +Consequentially, with a `Packed` storage layout, any message interacting with the contract +storage will always need to operate on the entire contract storage struct. + +For example, if we have a somewhat small contract storage struct consisting of only a few +tiny fields, pulling everything from the storage inside every message is not +problematic. It may even be advantageous - especially if we expect most messages to +interact with most of the storage fields. + +On the other hand, this can get problematic if we're storing a large `ink::prelude::vec::Vec` +in the contract storage but provide messages that do not need to read and write from this +`Vec`. In that scenario, each and every contract message bears runtime overhead by dealing +with that `Vec`, regardless whether they access it or not. This results in extra gas costs. +To solve this problem we need to turn our storage into a non-packed layout somehow. + +:::caution + +If any type exhibiting `Packed` layout gets large enough (an ever growing `Vec` might be +a prime candidate for this), it will break your contract. +This is because for encoding and decoding storage items, there is a buffer with only limited +capacity (around 16KB in the default configuration) available. This means any contract +trying to decode more than that will trap! If you are unsure about the potential size a +datastructure might get, consider using an ink! `Mapping`, which can store an arbitrary +number of elements, instead. + +::: + +## Eager Loading vs. Lazy Loading +ink! provides means of breaking the storage up into smaller pieces, which can be loaded +on demand, with the +[`Lazy`](https://paritytech.github.io/ink/ink/storage/struct.Lazy.html) primitive. +Wrapping any storage field inside a `Lazy` struct makes the storage +struct in which that field appears also +non-`Packed`, preventing it from being eagerly loaded during arbitrary storage operations. + +The following example demonstrates how we can solve the problem introduced in the above +section. You'll notice that for the lazily loaded storage field, we now work with getters +and setters to access and modify the underlying storage value: + +```rust +#![cfg_attr(not(feature = "std"), no_std)] + +#[ink::contract] +mod mycontract { + use ink::prelude::vec::Vec; + use ink::storage::Lazy; + + #[derive(Default)] + #[ink(storage)] + pub struct MyContract { + tiny_value: Balance, + /// This vector might get large and expensive to work with. + /// We want to enforce a non-`Packed` storage layout. + large_vec: Lazy>, + } + + impl MyContract { + #[ink(constructor)] + pub fn new() -> Self { + Self::default() + } + + /// Because `large_vec` is loaded lazily, this message is always cheap. + #[ink(message)] + pub fn get_balance(&self) -> Balance { + self.tiny_value + } + + /// Lazy fields like `large_vec` provide `get()` and `set()` storage operators. + #[ink(message)] + pub fn add_balance(&mut self, value: Balance) { + let mut balances = self.large_vec.get_or_default(); + balances.push(value); + self.large_vec.set(&balances); + } + } +} +``` + +:::caution + +`ink::prelude::vec::Vec`'s are always loaded in their entirety. This is because all elements +of the `ink::prelude::vec::Vec` live under a single storage key. Wrapping the +`ink::prelude::vec::Vec` inside `Lazy`, like the +provided example above does, has no influence on its inner layout. If you are dealing with +large or sparse arrays on contract storage, consider using a `Mapping` instead. + +::: + +## Manual vs. Automatic Key Generation + +By default, keys are calculated automatically for you, thanks to the +[`AutoKey`](https://docs.rs/ink_storage_traits/4.0.0-beta.1/ink_storage_traits/struct.AutoKey.html) +primitive. They'll be generated at compile time and ruled out for conflicts. +However, for non-`Packed` types like `Lazy` or the `Mapping`, the +[`ManualKey`](https://docs.rs/ink_storage_traits/4.0.0-beta.1/ink_storage_traits/struct.ManualKey.html) +primitive allows manual control over the storage key of a field like so: + +```rust +#[ink(storage)] +pub struct MyContract { + /// The storage key for this field is always `0x0000007f` + inner: Lazy>, +} +``` + +This may be advantageous: Your storage key will always stay the same, regardless of +the version of your contract or ink! itself (note that the key calculation algorithm may +change with future ink! versions). + +:::tip + +Using `ManualKey` instead of `AutoKey` might be especially desirable for upgradable +contracts, as using `AutoKey` might result in a different storage key for the same field +in a newer version of the contract. This may break your contract after an upgrade 😱! + +::: + +## Considerations + +It might be worthwhile to think about the desired storage layout of your contract. While +using a `Packed` layout will keep your contracts overall code size smaller, it can cause +unnecessarily high gas costs. Thus we consider it a good practice to break up large +or complex storage layouts into reasonably sized distinct storage cells. + +:::note + +ink! `Mapping`s are always non-`Packed` and loaded lazily, one key-value pair at the time. + +::: diff --git a/versioned_sidebars/version-4.0.0-alpha.1-sidebars.json b/versioned_sidebars/version-4.0.0-alpha.1-sidebars.json index 94003fefb8..b48e623014 100644 --- a/versioned_sidebars/version-4.0.0-alpha.1-sidebars.json +++ b/versioned_sidebars/version-4.0.0-alpha.1-sidebars.json @@ -59,7 +59,7 @@ "Storage & Data Structures": [ "datastructures/overview", "datastructures/mapping", - "datastructures/spread-storage-layout", + "datastructures/storage-layout", "datastructures/custom", "datastructures/storage-in-metadata" ], @@ -83,4 +83,4 @@ "brand-assets/cargo-contract" ] } -} +} \ No newline at end of file