Description
Most of the existing storage types were removed because they are simply to inefficient in size and executed instructions. Right now we only have Mapping
. However, this type doesn't work very well with the existing storage infrastructure and needs a band aid named SpreadAllocate
and initialize_contract
.
We want to pivot more in the direction of how storage works in FRAME. We should keep the overarching contract data structure but use the storage macro to generate the supporting code instead of a elaborate system of traits.
This would also allow us more flexibility in how to generate the storage keys for these data structures and more control over what code is generated. The aim is to reduce the contract sizes by removing monomorphization and copying of the root keys.
Please note that the following is only a sketch of the generated code. It is probably not how the generated code should look like. @Robbepop noted that we should not use private sub modules. Please feel free to post your suggestions and I will update the top post.
We add a new trait that will be implemented by the storage macro to supply our storage traits with a key. Our storage traits have default implementations for every function. The macro will only set the associated types.
trait HasKey {
const KEY: [u8; 32];
}
trait StorageValue: HasKey {
type Value: Encode + Decode;
// we take self here to build in the mutability levels
fn write(&mut self, value: &Value) {
ink_env::set_contract_storage(Self::KEY, value.encode());
}
fn read(&self) -> Self::Value {
ink_env::get_contract_storage(Self::KEY)
}
}
trait StorageMap: HasKey {
type Key: Hashable;
type Value: Encode + Decode;
fn insert(&mut self, key: Self::Key, &value: Self::Value) {
ink_env::set_contract_storage(twoxx(Self::KEY ++ key), value.encode());
}
fn get(&self, key: Self::Key) -> Self::Value {
ink_env::get_contract_storage(twoxx(Self::KEY ++ key));
}
}
Then the storage macro creates a private module where it dumps all the generated code:
#[ink(storage)]
struct Contract {
a: u8,
b: Mapping<AccountId, Balance>,
}
// The macro would transfrom the struct into this:
// The underscores are just to prevent clashes with user defined modules
// Having public fields in the struct would be an error.
mod __private__ {
pub struct AFieldStorageValue {
/// prevent instantation outside of this generated code
_private: ()
}
pub struct BFieldMapping {
/// prevent instantation outside of this generated code
_private: ()
}
impl HasKey for AFieldStorageValue {
// hash of "a: AFieldStorageValue"
const KEY: [u8; 32] = b"3A9ADFE4234B0475EB1767ACD1BCFA75D7B9E0BA1C427FBAF0F476D181D3A820";
}
impl HasKey for BFieldMapping {
// hash of "b: BFieldMapping"
const KEY: [u8; 32] = b"33D27C398EEEA3851B750A1152FF9B63B1D6D10BE18E2D58A745776D9F2FF241";
}
// the functions all have default operations. We just need to set the types.
impl StorageValue for AFieldStorageValue {
type Value = u32;
}
// the functions all have default operations. We just need to set the types.
impl StorageMap for BFieldMapping {
type Key = AccountId;
type Value = Balance;
}
// fields are private cannot be contstructed by user
pub struct Contract {
a: AFieldStorageValue,
b: BFieldMapping,
}
impl Contract {
// we generate this but leave out any ink! defined collection types
// initialization is and always be the crux of the matter. I would
// suggest forcing the user to initialize everything here but
// collections. So every user defined type would be wrapped into
// a `StorageValue` and added here. The rest are ink! collections
// which might or might not be skipped here.
pub fn initialize(a: u32) -> Self {
let ret = Self {
a: AFieldStorageValue { _private: () },
b: BFieldMapping { _private: () },
};
ret.a.write(a);
ret
}
// we create accessors for each field with the correct mutability
// this way we keep the natural way of marking messages as
// mutable and getters
pub fn a(&self) -> &AFieldStorageValue { return &self.a };
pub fn b(&self) -> &BFieldMapping { return &self.b };
pub fn a_mut(&mut self) -> &mut AFieldStorageValue { return &mut self.a };
pub fn b_mut(&mut self) -> &mut BFieldMapping { return &mut self.b };
}
}
use __private__::Contract;
impl Contract {
// Should we have the `constructor` macro write the `Contract` on `Drop`?
#[ink(constructor)]
pub fn new() -> Self {
// user cannot fuck up because it is the only constructor available
Self::initialize(42)
}
#[ink(message)]
pub fn do_writes(&mut self) {
self.a_mut().write(1);
self.b_mut().insert(ALICE, 187334343);
}
#[ink(message)]
pub fn do_reads(&self) {
let a = self.a().read();
let b = self.b().get(ALICE);
}
#[ink(message)]
pub fn do_reads_and_writes(&mut self) {
// just works
self.a_mut().write(1);
self.b_mut().insert(ALICE, 187334343);
let a = self.a().read();
let b = self.b().get(ALICE);
}
}
The biggest downside of this approach is the same as substrate's: New storage collections always need changes to be made in codegen in order to support them. However, I assume this will safe us a lot of code passing around all those keys and stateful objects (lazy data structures) at runtime. It is also much simpler and easier to reason about (because it has less features).
Open Questions
How does this affect/play with things like:
- Multi-file contracts?
- ink-as-dependency contracts
- ink! traits