-
Notifications
You must be signed in to change notification settings - Fork 428
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 rework #1217
Storage rework #1217
Conversation
…torage_item` macro to generate implementation of all traits and use a right storage types for `StorageMapping` and `StorageValue`.
Erc20 and Erc1155 works with a new storage. The size reduced ~550 bytes
The change already works for The size reduction: Example of storage layout of "storage": {
"struct": {
"fields": [
{
"layout": {
"cell": {
"key": "0x00000123",
"ty": 0
}
},
"name": "total_supply"
},
{
"layout": {
"cell": {
"key": "0xd44dbd33",
"ty": 1
}
},
"name": "balances"
},
{
"layout": {
"cell": {
"key": "0x429121b3",
"ty": 9
}
},
"name": "allowances"
}
]
}
}, The change has several differences from what we discussed. We added If the user wants to use the non-atomic struct twice or more he should specify a generic with /// A simple ERC-20 contract.
#[ink(storage)]
#[derive(Default)]
pub struct Erc20<KEY: StorageKeyHolder = ManualKey<0x123>> {
/// Total token supply.
total_supply: Balance,
/// Mapping from owner to number of owned token.
balances: StorageMapping<AccountId, Balance>,
/// Mapping of the token amount which an account is allowed to withdraw
/// from another account.
allowances: StorageMapping<(AccountId, AccountId), Balance>,
} That feature also is useful for upgradable contracts t specify the storage key of the upgradable struct. Also in #[ink_lang::storage_item]
struct Atomic {
s1: u128,
s2: Vec<u128>,
// Fails because `StorageType` implemented only for `Vec` where T: AtomicGuard<true>
// s3: Vec<NonAtomic>,
}
#[ink_lang::storage_item]
struct NonAtomic {
s1: StorageMapping<u128, u128>,
s2: StorageValue<u128>,
}
#[ink_lang::storage_item]
struct Jora<KEY: StorageKeyHolder> {
s1: StorageMapping<u128, u128>,
s2: StorageValue<u128>,
s3: StorageMapping<u128, Atomic>,
s4: StorageValue<NonAtomic>,
// Fails because: the trait `AtomicGuard<true>` is not implemented for `NonAtomic`
// s5: StorageMapping<u128, NonAtomic>,
} In the comment, you can see how it is expanded. We need to get a feedback from you to work in direction of finilization of the change. |
We've also commented out on a lot of stuff in |
IMO we can remove these (and in turn Also they are currently internal and not exposed as part of the public API. As a future task we can revisit these legacy lazy/collection types to see whether it is worth adapting them to the new storage abstractions in terms of code size/gas costs. |
Could you quickly recap why In a next iteration we can introduce a new I agree with @ascjones that we can remove From our pov it looks like the right direction, so it's fine if you go ahead with adding docs and tests. |
We agreed on that in the issue. The chance of collision is low and it is still optimal for us. We don't need
It will be resolved by paritytech/substrate#11029. I meant that the size will be reduced more in the future, but right now it is
Okay, cool, thanks for review=) |
The next version will take a ptr/len pair. So arbitrary length. ink! will use |
Removed all lazy stuff except `StorageMapping` and `StorageValue`. Later I will rename `StorageMapping` into `Mapping`. Removed `AtomicStatus` trait. Instead of it added `is_atomic!` macro that returns true if the object implements `AtomicGuard<true>`. It solves the problem when the struct contains generics. Now if the generic is marked as `AtomicGuard<true>` then the struct is atomic too, else it is not atomic. Implemented `#[ink_lang::storage_item]` for structs, enums, and unions. Started cleaning of the `SpreadLayout` and other stuff. The dispatching codegen already works without it.
# Conflicts: # crates/storage/src/lazy/mapping.rs # crates/storage/src/traits/optspec.rs
…ng` are not atomic
Created derives for a new traits. Added tests for derive. Added experimental `StorageType2` and auto-selecting of the storage key. Updated `storage_item` to be configurable and allow disabling auto derive for manual implementation.
The current implementation of the trait that returns the type with the right storage key looks like this: /// Returns the type that should be used for storing the value
pub trait StorageType<Salt: StorageKeyHolder> {
/// Type with storage key inside
type Type;
} It works well if we don't want to support generics in structures that can be part of the storage. Because we can't define that kind of struct. // Compilation error because `Salt` is not part of the struct + we will use another `ManualKey`
struct Generic<T: StorageType<Salt>, Salt: StorageKeyHolder> {
s1: u128,
s2: u32,
s3: T,
} I created a new StorageType2 trait with upcoming stabilization of GATs. And it helped to simplify the code with auto storage key selection, simplify codegen, and now the usage of the trait is very user-friendly. That allows us to define the structs and enums with generic now. We still have a problem with the auto-generated I played with it, and it is better than before, but still, I have issues with We can create a fully generic private structure, that structure can have a lot of bounds from the derive. After that, we create an alias to that structure with the same name and same generics. It uses already actual types there. The user defines the next struct: #[ink_lang::storage_item]
#[derive(Default, Debug)]
struct Struct<T: StorageType + core::any::Any> {
s1: u32,
s2: u64,
s3: u128,
s4: T,
}
#[allow(non_snake_case)]
mod private_Struct {
use super::*;
#[derive(Default, Debug)]
pub struct Struct<A: StorageType, B: StorageType, C: StorageType, D: StorageType> {
pub s1: A::Type<ManualKey>,
pub s2: B::Type<ManualKey>,
pub s3: C::Type<ManualKey>,
pub s4: D::Type<ManualKey>,
}
impl<A: StorageType, B: StorageType, C: StorageType, D: StorageType> IsAtomic for Struct<A, B, C, D>
where
A: IsAtomic,
B: IsAtomic,
C: IsAtomic,
D: IsAtomic,
{}
}
#[allow(type_alias_bounds)]
type Struct<T: StorageType + core::any::Any> = private_Struct::Struct<u32, u64, u128, T>; For the user, it is the same type, but for us, we applied all bounds that we need. It also allows us to improve the implementation of the If we believe that GATs would be stabilized, I will use the implementation with GATs and remove the old one. What do you think about the idea of a private struct?
Cons:
The idea with a private struct can be implemented without GATs, but that means that we will not support generics=) If you are okay with that idea I will implement it. |
Do we really need to support generics for this first iteration? The current storage struct does not allow generics so just to replicate that would be good enough, and we can add that as future enhancement?
I don't know whether we can count on that, there still seems to be some contention in the stabilization PR, and it has been "coming soon" for a long time. I personally think it is a really nice feature but I'm wary of depending upon unstable features, there should be a very strong justification.
Legacy versions of At the very least we should consider if it is possible to use the same technique in this situation. I still need to spend more time looking into your WIP here to get my head fully around it, in order to provide some more in depth feedback. |
It is not only about the storage of the contract it is about the The workaround: implement all traits manually. So it is not critical, and we can do that in follow-up PR. If something is implemented wrongly, the user can still have bugs, but we can handle it by documentation.
Okay, then let's integrate GATs in follow-up PR when it will be stabilized=) |
Added `OnCallInitializer` with `pull_or_init!` for upgradeable case. Renamed `StorageMapping` -> `Mapping`. Added some comments adn updated tests.
Fixed tests. Added tests for `StorageType`. Updated comments. Renamed `AutomationStorageType` into `AutoStorageType`
I'm okay with doing this for the first iteration. My preference is to get something as simple as possible working from this PR, then iterate upon that. Then it will be easier to review and there will be faster feedback. |
Yep, I got it=) I've already updated everything to use |
# Conflicts: # crates/storage/src/collections/vec/tests.rs
# Conflicts: # crates/storage/src/traits/impls/prims.rs
Co-authored-by: Michael Müller <mich@elmueller.net>
Co-authored-by: Michael Müller <mich@elmueller.net>
Alright, so imo this PR is too thorny to review in its current state. It touches too many If we ever want to get this merged we need to make it easily reviewable. I suggest we set By stacking I mean something like:
Some blog posts about stacked PRs
You obviously know the PR better than we do, but here's the order of the stack I would
If some of these PRs are 50 lines long, that's fine. If they're like 2500, please find a Thanks again for you work on this 🙇 |
# Conflicts: # crates/lang/codegen/Cargo.toml # crates/lang/macro/Cargo.toml # examples/mother/lib.rs
Improved error for `Storable` derive
Moved to #1331 |
Implements #1134
Overview
It is an implementation of new storage of smart contracts. Previously each field was stored in its storage cell under its storage key. The storage key was calculated in runtime over fields iteration. In a new version, all atomic fields are stored in one storage cell under one storage key. All non-atomic fields know their storage key during compilation time.
SpreadLayout
,PacketLayout
,SpreadAllocate
,PacketAllocate
, and all related code.Removed(Removedink_storage::collection
andink_storage::lazy
. A new collections/lazy should be added in follow-up PRscollection
andlazy
from storage #1271).Introduced new traits:
StorageType
,StorageKeyHolder
,AtomicGuard
. All those traits are implemented for primitives and base collections.StorageType
andStorageKeyHolder
is used by a new#[ink_lang::storage_item]
macro to automate storage key calculation during compilation.AtomicGuard
prevents the usage of non-atomic structs/enums in collections or mappings. It is a guard for the user to not corrupt the storage.AtomicGuard
can be derived for the structure or enum if all fields are atomic(it can be implemented manually. You can do that if you sure that all types are atomic).The
scale::Encode
andscale::Decode
is used to pull and push storage.The
StorageKey
isu32
. Due to some limitations of the host functions, we still use the[u8; 32]
storage key in some places but it should be fixed with paritytech/substrate#11029.Atomic vs Non-atomic
The field is atomic if it doesn't require its storage cell.
New
ink_storage::Mapping
andink_storage::StorageValue
allow to occupy additional storage cells. The storage key is calculated for those types during the compilation time. The rules about automatic storage key calculation you can find below. By default, those types use theink_storage::traits::AutoKey
policy, but anyone can specify that a storage key should be used viaink_storage::traits::ManualKey
like:The specified storage key is more prior than autogenerated. Any struct or enum, that uses those types, becomes non-atomic(more non-atomic types in follow-up PRs).
#[ink_lang::storage_item]
#[ink_lang::storage_item]
should be used during the declaration of a new struct or enum if it should be part of the storage. The macro derives all required traits by default:But it can be disabled via the
derive
argument to implement them manually.If you declared and planned to use a non-atomic type twice or more, you need to declare it with a generic storage key
KEY: StorageKeyHolder
. It will propagate the storage key from parent to child to prevent the same storage key.The macro expects
StorageKeyHolder
naming. It is hardcoded and you can't use aliases.Storage key rules
Right now hashing is used
SHA2
because it allows to do it during compilation time.Maybe in the future, it will be changed if
Blake2b
will support const calculation.The rules to calculate the storage key of the field:
struct_name
and call itS
.variant_name
isSome
then computes the ASCII byte representation and call itV
.field_name
and call itF
.S
andF
) or (S
,V
andF
) using::
as separator and call itC
.SHA2
256-bit hashH
ofC
.H
make up the storage key.variant_name
isNone
for structures and unions.field_name
is"{}"
where{}
is a number of the field("0", "1" ...).If the parent's storage key is passed then use the next rules: If one of the keys is zero, then return another
without hashing. If both keys are non-zero, return the hash of both keys.
If the type is a tuple, then it uses additional rules to calculate the inner storage key:
::
.A
,B
, ...,J
are hardcoded.The final:
For
(A)
it is(A)::0
For
(A, B)
it is(A, B)::0
,(A, B)::1
...
For
(A, B, ..., J)
it is(A, B, ..., J)::0
,(A, B, ..., J)::1
, ...,(A, B, ..., J)::9
Contract customizability
ContractRootKey
trait.ManualKey
for the contract's storage. Now anyone can specify the default storage key of the contract.OnCallInitializer
. The contract can implement that trait to support initialization on the runtime if it is unable to pull from the storage.It can be in several cases:
Proxy
orDiamond
pattern.If the trait is not implemented the behavior of the storage is the default - It should be first initialized by the constructor.
Metadata
The metadata was reworked to be fully compatible with storage key generation rules. The storage layout now uses the
u32
layout key instead of[u8; 32]
. Each struct or enum layout has a name and all unnamed fields have a enumerate name like "0", "1" etc. Now everyone can calculate the storage key based on the naming that was used. The metadata still is version 3, it should be updated to version 4 in #1265.Before the generation of the metadata, it checks that the metadata is valid. Right now it can be invalid if some storage keys intersect(maybe more errors in the future).
The conflict storage key error looks like:
The storage layout example (it's pretty large, so it's hidden by default):
Follow-ups
StorageType
and use generics salt in the associated type.#[ink_lang::storage_item]
may have problems with bounds for inner types. We can implement the approach from that comment.Lazy
non-atomic type, that allows loading of the storage type in a lazy manner.[u8; 32]