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

Storage refactoring new hope for review #1331

Merged

Conversation

xgreenx
Copy link
Collaborator

@xgreenx xgreenx commented Jul 20, 2022

We decided to abandon old PR because it contains a lot of commits and comments and is hard to review.

The whole change was split into seven commits with their descriptions. In most cases, it is crate-based splitting. But some crates united together because they related to the same area.

You could check first the commit six with updated examples. It will help to understand how the usage changed. But after better to go in order of commits.
Commits:

  1. First commit imports new unstable functions with transparent hashing.
    Updated the API to work with the generic key K: Encode instead of the old Key.
    Also, the change contains optimization to reduce the size of contracts. In most cases, it is #[inline(always)]; but return_value also got small optimization; removed usage of extract_from_slice where it is not needed.

  2. Second commit:

    • primitives crate:
      Removed old 32 bytes Key. Replaced it with u32. Added KeyComposer, it is a helper struct that does all manipulation with the storage key. It allows concat two storage keys into one, compute storage key for fields based on the filed, struct, enum, variants names.
      Removed all tests and benches. Didn't add it for new primitives because the key is standard u32, and all keys are calculated during compilation.

    • storage crate:
      Removed SpreadLayout, PackedLayout, SpreadAllocate, PackedAllocate, and all related helper functions.
      Removed Packed struct cause it is the default behavior for storage right now. Added Lazy struct that allows get/set value from/into the storage. It is similar to Mapping but works with one storage key and one value.
      Introduced new main traits to work with storage in storage/src/traits/storage.rs.
      Also added a new OnCallInitializer trait to improve the flow with upgradable contracts and support initialization on demand by default. Added pull_or_init macro that inits the storage struct if it is impossible to load it. It also can be used as optimization for contracts without an explicit constructor.
      Replaced implementation of old traits for main primitives with a new one. Added blanket implementation of new traits for structures that are Packed by default. It reduces the amount of code and adds support of generic structures but adds problems with tuples(now tuples implement new traits only if inner items are Packed).
      Introduced AutoKey and ManualKey that allows specifying which key the user wants to use. Added support of it into all traits and structures.
      Refactored Mapping to follow new rules.

  3. Third commit:

    • metadata crate:
      Updated storage layout in the metadata. Introduces the concept of roots and leafs. Root defines the storage key for all sub-tree until there is another Root. Leafs are common types that are part of the sub-tree and serialized/deserialized together into one storage key.
      Replaced 32 bytes storage key with u32.
      Added validation that all root storage keys don't overlap. Maybe better to add that error or reuse that validator on the cargo-contract side to show a more user-friendly error.
      RootLayout and validator are used in codegen(next commit). The contract is wrapped into RootLayout, and we do validation for that tree.
      Metadata now contains name for each struct/enum/variant/field. It is useful information because now the storage key is calculated based on the name of struct/enum/variant/field.

    • storage crate:
      Added a new helper crate storage/codegen. It contains some useful functional that is used in ink_storage_derive and in ink_lang_codegen crates. It has a method that returns all types of the struct/enum/unit and a method that finds "salt" in the generics of the struct/enum. It uses the magic constant "KeyHolder"(about that you can read in the issue) to find salt, so I tried to have only one place where we are using that constant.

    • storage derive crate:
      Replaced derive implementation of old trait with a new one. You can check the tests to see what the implementation looks like. Storable recursively calls encode and decode for all fields. KeyHolder return the key of the salt. Item uses AutoKey or a key specified by the user. I want to highlight that PreferredKey only is used with the AutoItem trait. If PreferredKey is AutoKey, then AutoItem<AutoGenerated> select auto-generated key, otherwise preferred. So AutoItem trait decides which key to use. It is why derive macro only set PreferredKey.
      Updated drive for StorageLayout, now it uses u32 and passes the name of the struct into metadata.

  4. Fourth commit removed initialize_contract and related to initialization stuff. Now the codegen uses pull_or_init in the call. Updated execute_constructor to work with new storage methods.
    Allowed usage of generics during the declaration of the primary contract storage. Users can specify the default storage key with KeyHolder via generic.
    Added parser for storage_item macro with its config.

  5. Fifth commit removed the old codegen related to spread and packed layout. If some object implements Decode and Encode, it is Packed, and it uses the blanket implementation of new traits.
    In dispatch, codegen started to use a new method to work with storage.
    In metadata codegen added usage of new RootLayout. We wrap the contract into that layout because the contract has its storage key for all inner fields by default. Also added a run of validation logic during metadata generation.
    Added storage_item macro. It transforms all types from autokey into manual key(if types support it). It calculates the storage key based on the name(it uses the KeyComposer::compute_key function from the primitives crate). Also, macro generates an additional Check structure that includes all raw fields. It helps show the user correct errors in typos, wrong types, etc.

  6. Sixth commit updates examples to sue a new API.

  7. Seventh commit adds new UI tests and fixes the old one.

polkadot address: 1nNaTpU9GHFvF7ZrSMu2CudQjXftR8Aqx58oMDgcuoH8dKe

@xgreenx xgreenx force-pushed the feature/storage-rework-new-hope branch 3 times, most recently from aa358eb to 3d969bf Compare July 20, 2022 18:39
Updated the API to work with the generic key `K: Encode` instead of the old `Key`.
Also, the change contains optimization to reduce the size of contracts. In most cases, it is `#[inline(always)]`; but `return_value` also got small optimization; removed usage of `extract_from_slice` where it is not needed.
Removed old 32 bytes `Key`. Replaced it with `u32`. Added `KeyComposer`, it is a helper struct that does all manipulation with the storage key. It allows concat two storage keys into one, compute storage key for fields based on the filed, struct, enum, variants names.
Removed all tests and benches. Didn't add it for new primitives because the key is standard `u32` and all keys are calculated during compilation.

storage crate:
Removed `SpreadLayout`, `PackedLayout`, `SpreadAllocate`, `PackedAllocate`, and all related helper functions.
Removed `Packed` struct cause it is the default behavior for storage right now. Added `Lazy` struct that allows `get`/`set` value from/into the storage. It is similar to `Mapping` but works with one storage key and one value.
Introduced new main traits to work with storage in `storage/src/traits/storage.rs`.
Also added a new `OnCallInitializer` trait to improve the flow with upgradable contracts and support initialization on demand by default. Added `pull_or_init` macro that inits the storage struct if it is impossible to load it. It also can be used as optimization for contracts without an explicit constructor.
Replaced implementation of old traits for main primitives with a new one. Added blanket implementation of new traits for structures that are `Packed` by default. It reduces the amount of code and adds support of generic structures but adds problems with tuples(now tuples implement new traits only if inner items are `Packed`).
Introduced `AutoKey` and `ManualKey` that allows specifying which key the user wants to use. Added support of it into all traits and structures.
Refactored `Mapping` to follow new rules.
Updated storage layout in the metadata. Introduces the concept of roots and leafs. Root defines the storage key for all sub-tree until there will be another Root. Leafs are common types that are part of the sub-tree and serialized/deserialized together into one storage key.
Replaced 32 bytes storage key with `u32`.
Added validation that all root storage keys don't overlap. Maybe better to add that error or reuse that validator on the `cargo-contract` side to show a more user-friendly error.
`RootLayout` and validator are used in codegen(next commit). The contract is wrapped into `RootLayout`, and we do validation for that tree.
Metadata now contains name for each struct/enum/variant/field. It is useful information because now the storage key is calculated based on the name of struct/enum/variant/field.

storage crate:
Added a new helper crate `storage/codegen`. It contains some useful functional that is used in `ink_storage_derive` and in `ink_lang_codegen` crates. It has a method that returns all types of the struct/enum/unit and a method that finds "salt" in the generics of the struct/enum. It uses magic constant "KeyHolder"(about that you can read in issue) to find salt, so I tried to have only one place where we are using that constant.

Replaced derive implementation of old trait with new one. You can check the tests to see how the implementation looks like. `Storable` recursively call `encode` and `decode` for all fields. `KeyHolder` return key of the salt. `Item` uses `AutoKey` or key specified by the user. I want to highlight that `PreferredKey` only is used with the `AutoItem` trait. If `PreferredKey` is `AutoKey`, then `AutoItem<AutoGenerated>` select auto-generated key, otherwise preferred. So `AutoItem` trait decides that key to use. It is why derive macro only set `PreferredKey`.
Updated drive for `StorageLayout`, now it uses `u32` and pass name of the struct into metadata.
…w the codegen uses `pull_or_init` in the `call`. Updated `execute_constructor` to work with new storage methods.

Allowed usage of generics during the declaration of the primary contract storage. Users can specify the default storage key with `KeyHolder` via generic.
Added parser for `storage_item` macro with its config.
…object implements `Decode` and `Encode`, it is `Packed`, and it uses the blanket implementation of new traits.

In dispatch, codegen started to use a new method to work with storage.
In metadata codegen added usage of new `RootLayout`. We wrap the contract into that layout because the contract has its storage key for all inner fields by default. Also added a run of validation logic during metadata generation.
Added `storage_item` macro. It transforms all types from autokey into manual key(if types support it). It calculates the storage key based on the name(it uses the `KeyComposer::compute_key` function from the primitives crate). Also, macro generates an additional `Check` structure that includes all raw fields. It helps show correct errors to the user in case of typos, wrong types, etc.
Simplified delegate call example very well causes of new `OnCallInitializer` trait and support of manual specifying key.
Can't highlight something unusual here=)
@xgreenx xgreenx force-pushed the feature/storage-rework-new-hope branch from 3d969bf to f967986 Compare July 21, 2022 13:46
@xgreenx xgreenx mentioned this pull request Jul 21, 2022
@HCastano HCastano added the A-ink_storage [ink_storage] Work Item label Jul 21, 2022
Copy link
Contributor

@HCastano HCastano left a comment

Choose a reason for hiding this comment

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

Yeah this is a lot easier to review!

So far I've only looked at the 1st and 6th commits (and took a peek at the expanded code), but will work through it this week

crates/env/src/engine/on_chain/buffer.rs Show resolved Hide resolved
crates/env/src/engine/on_chain/impls.rs Outdated Show resolved Hide resolved
let mut scope = self.scoped_buffer();
let enc_return_value = scope.take_encoded(return_value);
ext::return_value(flags, enc_return_value);
let mut scope = EncodeScope::from(&mut self.buffer[..]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we not use the append_encoded and take_appended methods from ScopedBuffer instead of using EncodeScope?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I didn't get your question=) We want to use EncodeScope because it implements scale::Output. The optimization is to do nothing with a buffer during result returning(It is the end of the execution, and we don't worry about memory validity after usage).

crates/env/src/engine/on_chain/ext.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/impls/storage.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/impls/storage.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/impls/storage.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/storage.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/storage.rs Outdated Show resolved Hide resolved

/// Automatically returns the type that should be used for storing the value.
///
/// Trait is used be codegen to use the right storage type.
Copy link
Contributor

Choose a reason for hiding this comment

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

"... used by codegen ..."

What does "correct" mean here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated:

The trait is used by codegen to determine which storage key the type should have.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should find a better name for AutoItem, something that reflects more what it's about. Maybe StorageType?

Regarding

The trait is used by codegen to determine which storage key the type should have.

The word type here is confusing to me. We don't derive the key from the type, no? If I look at fn convert_into_storage_field the type is not part of the key.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The word type here is confusing to me. We don't derive the key from the type, no? If I look at fn convert_into_storage_field the type is not part of the key.

The type defines which storage key we should use=)

// Here, the storage key is `123`
let storage: Contract<ManualKey<123>> = Default::default(); 
// Here, the storage key is `234`
let storage: Contract<ManualKey<234>> = Default::default();
// Here, the storage key is a hash of `234` and `123`
let storage: Contract<ManualKey<234, ManualKey<123> /* It is Salt or ParentStorageKey */ >> = Default::default();

I think we should find a better name for AutoItem, something that reflects more what it's about. Maybe StorageType?

StorageType is the previous name of the Item=) And previously, it was AutoStorageType. I'm okay with StorageType but it doesn't highlight that it is related to automatically type determination. Item helps to determine the type too, but it also contains PreferredKey

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What do you think about?:

pub trait Item<AutoGenerated: StorageKey> {
    type Type: Storable;
    type PreferredKey: StorageKey;
}

pub trait StorageType<AutoGenerated: StorageKey> {
    type Type: Storable;
}

or maybe simplify to Generated

crates/storage/src/traits/storage.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/storage.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/storage.rs Outdated Show resolved Hide resolved
crates/primitives/src/key.rs Show resolved Hide resolved
crates/primitives/src/key.rs Outdated Show resolved Hide resolved
crates/primitives/src/key.rs Outdated Show resolved Hide resolved
crates/primitives/src/key.rs Outdated Show resolved Hide resolved
crates/primitives/src/key.rs Outdated Show resolved Hide resolved
bytes[offset + 2],
bytes[offset + 3]
)?;
use ink_prelude::vec;
Copy link
Contributor

Choose a reason for hiding this comment

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

We should still have some tests here imo

crates/storage/src/traits/impls/storage.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/impls/storage.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/impls/mod.rs Outdated Show resolved Hide resolved
@cmichi cmichi mentioned this pull request Jul 28, 2022
crates/storage/src/test_utils.rs Outdated Show resolved Hide resolved
crates/storage/src/test_utils.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/layout/impls.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/pull_or_init.rs Outdated Show resolved Hide resolved
Comment on lines 42 to 48
pub trait PullOrInitFallback<T: Storable> {
#[allow(dead_code)]
fn pull_or_init(key: &Key) -> T {
pull_storage(key)
}
}
impl<T: Storable> PullOrInitFallback<T> for PullOrInit<T> {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this trait really necessary? Can you not implement this on PullOrInit directly? It looks like in the codegen you go through the PullOrInit struct anyways

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That trait provides a default implementation of the pull_or_init method if the contract doesn't implement the OnCallInitializer trait. The same if don't for is_result_type(IsResultTypeFallback).

It is a way how to get autoref specialization without specialization feature)

Comment on lines 50 to 60
/// Pulls the struct from the storage or creates and new one and inits it.
#[macro_export]
#[doc(hidden)]
macro_rules! pull_or_init {
( $T:ty, $key:expr $(,)? ) => {{
#[allow(unused_imports)]
use $crate::traits::pull_or_init::PullOrInitFallback as _;

$crate::traits::pull_or_init::PullOrInit::<$T>::pull_or_init(&$key)
}};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this macro is really necessary. It's only used once it the codegen, and a couple of times in the tests

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is a separate macro with its module because of tests. But tests check that everything works as expected=)

crates/storage/src/traits/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/lazy/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/lazy/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/lazy/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/lazy/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/lazy/mod.rs Outdated Show resolved Hide resolved
/// }
///
/// impl MyContract {
/// #[ink(constructor)]
/// pub fn new() -> Self {
/// ink_lang::utils::initialize_contract(Self::new_init)
/// let mut instance = Self::default();
/// instance.new_init();
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar thing here, I'd take the code in new_init and just fold it here. But again, if we just want to show how to use ManualKey do we even need the rest of the example?

crates/storage/src/lazy/mapping.rs Outdated Show resolved Hide resolved
crates/storage/src/lazy/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/lazy/mapping.rs Outdated Show resolved Hide resolved
@@ -154,7 +166,8 @@ where
where
Q: scale::EncodeLike<K>,
{
pull_packed_root_opt(&self.storage_key(&key))
ink_env::get_contract_storage::<(&Key, Q), V>(&(&KeyType::KEY, key))
.unwrap_or_else(|error| panic!("failed to get value in mapping: {:?}", error))
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.unwrap_or_else(|error| panic!("failed to get value in mapping: {:?}", error))
.unwrap_or_else(|error| panic!("Failed to get value in Mapping: {:?}", error))

I wonder if we can test this somehow. Maybe by clearing the underlying contract storage manually and then trying to pull. Really this shouldn't happen unless a storage migration messes up your storage layout

Copy link
Collaborator Author

@xgreenx xgreenx left a comment

Choose a reason for hiding this comment

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

Thank you @HCastano for the review=) I hope I didn't miss any of your comments.

I applied most of your suggestions locally, updated/added comments and tests. There are several major questions regarding naming let's discuss them and after I will push all changes.

  1. Hernando suggested renaming the KeyHolder trait into the StorageKey trait. During the previous review iteration, we decided to use the naming KeyHolder and we tried to remove all "Storage" prefixes for all entities in the ink_storage. The KeyHolder trait means that the type occupies some storage key. So StorageKey also fits well.

  2. The trait Item<X: KeyHolder> is used to manipulate the type's storage key. By default, the type will use the storage key passed as X. I called it Salt because in the implementation underhood it is used as a salt to generate the final storage key. But it is not accurate naming for public trait and may be better to name it "Key: StorageKey" or "KeyHolder: StorageKey", "ParentKey: StorageKey" or maybe another name(remember we can select a good name for bound too).

  3. Packed vs non-Packed: With the introduction of blanket implementation of the Packed trait, it simplified the implementation of Storable and other traits for primitives. We can simplify the previous structure as Hernando suggested. I used the old structure because we didn't decide if we wanted to have two macros, ink::packed and ink::non_packed. With two macros, maybe we don't want to have blanket implementation. Do we want to discuss it or are we okay with the current implementation and I will simplify the structure more?

  4. All functions from ink_env use scale::Decode and scale::Encode. ink_env doesn't know about ink_storage::traits::Storable. So here I created EncodeWrapper and DecodeWrapper to transform Storable into scale::Encode or scale::Decode. And it is a hack=) I tried not to break the abstraction that all traits related to storage in the ink_storage crate. What do you think, can we move the Storable definition to ink_env and use Storable for ink_env::set_contract_storage and get_contract_storage instead of scale::Decode and scale::Encode? And where we should define derive macro for Storable in that case?

crates/env/src/engine/on_chain/buffer.rs Show resolved Hide resolved
let mut scope = self.scoped_buffer();
let enc_return_value = scope.take_encoded(return_value);
ext::return_value(flags, enc_return_value);
let mut scope = EncodeScope::from(&mut self.buffer[..]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I didn't get your question=) We want to use EncodeScope because it implements scale::Output. The optimization is to do nothing with a buffer during result returning(It is the end of the execution, and we don't worry about memory validity after usage).

crates/storage/src/lazy/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/lazy/mod.rs Show resolved Hide resolved
crates/storage/src/test_utils.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/storage.rs Outdated Show resolved Hide resolved
///
/// The trait is automatically implemented for [`Packed`](crate::traits::Packed) types
/// via blank implementation.
pub trait Item<Salt: KeyHolder> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It was initially called Salt because it is only one part of the storage key, and in most cases, it is salt for the hash function=)

But Key or KeyHolder for the generic sounds better. Let's discuss about it in general

crates/storage/src/traits/storage.rs Outdated Show resolved Hide resolved

/// Automatically returns the type that should be used for storing the value.
///
/// Trait is used be codegen to use the right storage type.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated:

The trait is used by codegen to determine which storage key the type should have.

crates/storage/src/traits/storage.rs Outdated Show resolved Hide resolved
@@ -10,7 +10,7 @@ publish = false
[dependencies]
ink_primitives = { path = "../../../../crates/primitives", default-features = false }
ink_metadata = { path = "../../../../crates/metadata", default-features = false, features = ["derive"], optional = true }
ink_env = { path = "../../../../crates/env", default-features = false, features = ["ink-debug"] }
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this feature removal is unintentional.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed it to track the actual size of the contract

@HCastano
Copy link
Contributor

1. Hernando suggested renaming the `KeyHolder` trait into the `StorageKey` trait. During the previous review iteration, we decided to use the naming `KeyHolder` and we tried to remove all "Storage" prefixes for all entities in the `ink_storage`. The `KeyHolder` trait means that the type occupies some storage key. So `StorageKey` also fits well.

I don't mind having the Storage prefix in this case. It makes things clearer imo.

2. The trait `Item<X: KeyHolder>` is used to manipulate the type's storage key. By default, the type will use the storage key passed as `X`. I called it `Salt` because in the implementation underhood it is used as a salt to generate the final storage key. But it is not accurate naming for public trait and may be better to name it "Key: StorageKey" or "KeyHolder: StorageKey", "ParentKey: StorageKey" or maybe another name(remember we can select a good name for bound too).

So a big part of the confusion for me is that the Salt is only used after a few layers
of trait implementations, and only really populated in one specific case.

It's not used on Item directly, and not even on AutoItem directly. Instead we use the
Salt when implemeting KeyHolder for ManualKey, and then ManualKey gets used in
the implementation of AutoItem (which internally uses Item).

I think there's room for simplification around the traits and implementations
(Get rid of AutoItem? Roll the salting into KeyHolder? Implement ManualKeys later?),
but I'm a bit stuck on figuring out what the right move is here.

3. `Packed` vs non-`Packed`: With the introduction of blanket implementation of the `Packed` trait, it simplified the implementation of `Storable` and other traits for primitives. We can simplify the previous structure as Hernando suggested. I used the old structure because we didn't decide if we wanted to have two macros, `ink::packed` and `ink::non_packed`. With two macros, maybe we don't want to have blanket implementation. Do we want to discuss it or are we okay with the current implementation and I will simplify the structure more?

I'd stick with the current implementation. We can always have a follow up PR if we decide there
is a better approach.

4. All functions from `ink_env` use `scale::Decode` and `scale::Encode`. `ink_env` doesn't know about `ink_storage::traits::Storable`. So [here](https://github.com/paritytech/ink/pull/1331#discussion_r932509661) I created `EncodeWrapper` and `DecodeWrapper` to transform `Storable` into `scale::Encode` or `scale::Decode`. And it is a hack=) I tried not to break the abstraction that all traits related to storage in the `ink_storage` crate. What do you think, can we move the `Storable` definition to `ink_env` and use `Storable` for `ink_env::set_contract_storage` and `get_contract_storage` instead of `scale::Decode` and `scale::Encode`? And where we should define derive macro for `Storable` in that case?

Haven't gotten around to this yet, I'll need to think about it.

@xgreenx xgreenx requested a review from a team as a code owner August 15, 2022 16:43
crates/lang/codegen/src/generator/metadata.rs Outdated Show resolved Hide resolved
crates/lang/codegen/src/generator/storage_item.rs Outdated Show resolved Hide resolved
crates/storage/src/traits/mod.rs Show resolved Hide resolved
crates/storage/src/pull_or_init.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@ascjones ascjones left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Contributor

@HCastano HCastano left a comment

Choose a reason for hiding this comment

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

Green, thanks so much for this 💚! It's definitely been quite an effort on both sides, and
it's about time we get this into the hands of users. There's inevitably gonna be
follow up PRs as we discover bugs, edge cases, and quirks. Hopefully we catch
all the major ones before this hits a mainnet deployment 😅

Green, can you put down either a Polkadot or Kusama address in the PR description so
we can submit a tip for you?

@cmichi cmichi merged commit 5fc2d2a into use-ink:master Sep 5, 2022
@cmichi
Copy link
Collaborator

cmichi commented Sep 5, 2022

@xgreenx As Hernando wrote above: we want to give you a tip for the huge amount of work you've put into this PR (and the previous one from which this one was created + the smaller ones which were split off). In total you worked on this project for over six months, starting with the discussions in #1134, specifying it out. And then over four months ago you created the first PR and met with a number of people from the core team in person two times to discuss it.

Thank you so much! We're very happy to have you be part of our ecosystem!

Could you edit your description of this PR and add either one of those?

polkadot address: <SS58 Address>

or

kusama address: <SS58 Address>

@xgreenx
Copy link
Collaborator Author

xgreenx commented Sep 6, 2022

It's definitely been quite an effort on both sides, and
it's about time we get this into the hands of users.

Yea, we finally did it!=)

There's inevitably gonna be follow up PRs as we discover bugs,
edge cases, and quirks. Hopefully we catch all the major ones
before this hits a mainnet deployment 😅

I hope the number of problems will be as few as possible=) But definitely, we will have follow-up PRs to improve the feature, documentation, etc.

Thank you so much! We're very happy to have you be part of our ecosystem!

💚💚💚

@cmichi
Copy link
Collaborator

cmichi commented Sep 7, 2022

/tip large

@substrate-tip-bot
Copy link

@cmichi A large tip was successfully submitted for xgreenx (1nNaTpU9GHFvF7ZrSMu2CudQjXftR8Aqx58oMDgcuoH8dKe on polkadot).

https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc.polkadot.io#/treasury/tips

xermicus pushed a commit that referenced this pull request Sep 8, 2022
* Import new unstable functions with transparent hashing.
Updated the API to work with the generic key `K: Encode` instead of the old `Key`.
Also, the change contains optimization to reduce the size of contracts. In most cases, it is `#[inline(always)]`; but `return_value` also got small optimization; removed usage of `extract_from_slice` where it is not needed.

* primitives crate:
Removed old 32 bytes `Key`. Replaced it with `u32`. Added `KeyComposer`, it is a helper struct that does all manipulation with the storage key. It allows concat two storage keys into one, compute storage key for fields based on the filed, struct, enum, variants names.
Removed all tests and benches. Didn't add it for new primitives because the key is standard `u32` and all keys are calculated during compilation.

storage crate:
Removed `SpreadLayout`, `PackedLayout`, `SpreadAllocate`, `PackedAllocate`, and all related helper functions.
Removed `Packed` struct cause it is the default behavior for storage right now. Added `Lazy` struct that allows `get`/`set` value from/into the storage. It is similar to `Mapping` but works with one storage key and one value.
Introduced new main traits to work with storage in `storage/src/traits/storage.rs`.
Also added a new `OnCallInitializer` trait to improve the flow with upgradable contracts and support initialization on demand by default. Added `pull_or_init` macro that inits the storage struct if it is impossible to load it. It also can be used as optimization for contracts without an explicit constructor.
Replaced implementation of old traits for main primitives with a new one. Added blanket implementation of new traits for structures that are `Packed` by default. It reduces the amount of code and adds support of generic structures but adds problems with tuples(now tuples implement new traits only if inner items are `Packed`).
Introduced `AutoKey` and `ManualKey` that allows specifying which key the user wants to use. Added support of it into all traits and structures.
Refactored `Mapping` to follow new rules.

* metadata crate:
Updated storage layout in the metadata. Introduces the concept of roots and leafs. Root defines the storage key for all sub-tree until there will be another Root. Leafs are common types that are part of the sub-tree and serialized/deserialized together into one storage key.
Replaced 32 bytes storage key with `u32`.
Added validation that all root storage keys don't overlap. Maybe better to add that error or reuse that validator on the `cargo-contract` side to show a more user-friendly error.
`RootLayout` and validator are used in codegen(next commit). The contract is wrapped into `RootLayout`, and we do validation for that tree.
Metadata now contains name for each struct/enum/variant/field. It is useful information because now the storage key is calculated based on the name of struct/enum/variant/field.

storage crate:
Added a new helper crate `storage/codegen`. It contains some useful functional that is used in `ink_storage_derive` and in `ink_lang_codegen` crates. It has a method that returns all types of the struct/enum/unit and a method that finds "salt" in the generics of the struct/enum. It uses magic constant "KeyHolder"(about that you can read in issue) to find salt, so I tried to have only one place where we are using that constant.

Replaced derive implementation of old trait with new one. You can check the tests to see how the implementation looks like. `Storable` recursively call `encode` and `decode` for all fields. `KeyHolder` return key of the salt. `Item` uses `AutoKey` or key specified by the user. I want to highlight that `PreferredKey` only is used with the `AutoItem` trait. If `PreferredKey` is `AutoKey`, then `AutoItem<AutoGenerated>` select auto-generated key, otherwise preferred. So `AutoItem` trait decides that key to use. It is why derive macro only set `PreferredKey`.
Updated drive for `StorageLayout`, now it uses `u32` and pass name of the struct into metadata.

* Removed `initialize_contract` and related to initialization stuff. Now the codegen uses `pull_or_init` in the `call`. Updated `execute_constructor` to work with new storage methods.
Allowed usage of generics during the declaration of the primary contract storage. Users can specify the default storage key with `KeyHolder` via generic.
Added parser for `storage_item` macro with its config.

* Removed the old codegen related to spread and packed layout. If some object implements `Decode` and `Encode`, it is `Packed`, and it uses the blanket implementation of new traits.
In dispatch, codegen started to use a new method to work with storage.
In metadata codegen added usage of new `RootLayout`. We wrap the contract into that layout because the contract has its storage key for all inner fields by default. Also added a run of validation logic during metadata generation.
Added `storage_item` macro. It transforms all types from autokey into manual key(if types support it). It calculates the storage key based on the name(it uses the `KeyComposer::compute_key` function from the primitives crate). Also, macro generates an additional `Check` structure that includes all raw fields. It helps show correct errors to the user in case of typos, wrong types, etc.

* Updated all examples to use a new API.
Simplified delegate call example very well causes of new `OnCallInitializer` trait and support of manual specifying key.

* UI tests for a new codegen.
Can't highlight something unusual here=)

* Apply all suggestion from the review

* Make CI happy

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* Apply suggestions:
- In most cases it is comments=)
- Moved `pull_or_init` on one level upper.
- Put the tests into the `impls/mod.rs`

* Fix doc

* Add comment to autoref specialisation

* Suggestion from the review

* Revert back u8

* Remove unwrap

* Collapse if let

* Fixed overflow for enums

* Fixing comments

* Renamed `Item` to `StorableHint` and `AutoItem` to `AutoStorableHint`

* Fix test

* Renamed key_holder.
Add UI test for double storage_item.
Applied suggestion from the review.

* Nightly fmt

* Remove `Packed` path

* Fix doc test

* Apply suggestions from hte review

* Fixed build

* Fix build

* Removed `initialize_contract` from linting and deleted all tests

* Fix doc link

* Fix mapping example

* Applied suggestion.
Removed `delegate-call` example with `OnCallInitializer`

* Removed `delegate-calls` from the CI. Replaced it with `set-code-hash`

* fix test

* fix test

* Fix CI to use stable for contract build

* Fix CI to use stable for examples
This was referenced Sep 20, 2022
@ascjones ascjones mentioned this pull request Feb 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ink_storage [ink_storage] Work Item
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants