Skip to content

Commit

Permalink
Simple Mapping Storage Primitive (#946)
Browse files Browse the repository at this point in the history
* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* Simple Mapping type improvements (#979)

* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* adjust the Mapping type for the new SpreadAllocate trait

* adjust ERC-20 example for changes in Mapping type

* remove commented out code

* add doc comment to new_init

* make it possible to use references in more cases with Mapping

* use references in more cases for ERC-20 example contract

* remove unnecessary references in Mapping methods

* refactor/improve pull_packed_root_opt utility method slightly

* fix ERC-20 example contract

The problem with *self.total_supply is that it may implicitly read from storage in case it has not yet read a value from storage whereas Lazy::set just writes the value to the Lazy instance.

Co-authored-by: Hernando Castano <[email protected]>
Co-authored-by: Hernando Castano <[email protected]>

* Use new `initialize_contract()` function

* Derive `SpreadAllocate` for `ink(storage)` structs

* Stop manually implementing SpreadAllocate for ERC-20

* Stop implementing `SpreadAllocate` in the storage codegen

* Derive `SpreadAllocate` manually for ERC-20

* RustFmt example

* Move `Mapping` from `collections` to `lazy`

* Remove extra `0` in docs

Co-authored-by: Robin Freyler <[email protected]>
  • Loading branch information
HCastano and Robbepop authored Nov 15, 2021
1 parent ec7d5e8 commit 7432565
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 41 deletions.
189 changes: 189 additions & 0 deletions crates/storage/src/lazy/mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Copyright 2018-2021 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! A simple mapping to contract storage.
//!
//! # Note
//!
//! This mapping doesn't actually "own" any data.
//! Instead it is just a simple wrapper around the contract storage facilities.

use crate::traits::{
pull_packed_root_opt,
push_packed_root,
ExtKeyPtr,
KeyPtr,
PackedLayout,
SpreadAllocate,
SpreadLayout,
};
use core::marker::PhantomData;

use ink_env::hash::{
Blake2x256,
HashOutput,
};
use ink_primitives::Key;

/// A mapping of key-value pairs directly into contract storage.
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
#[derive(Default)]
pub struct Mapping<K, V> {
offset_key: Key,
_marker: PhantomData<fn() -> (K, V)>,
}

impl<K, V> core::fmt::Debug for Mapping<K, V> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("Mapping")
.field("offset_key", &self.offset_key)
.finish()
}
}

impl<K, V> Mapping<K, V> {
/// Creates a new empty `Mapping`.
fn new(offset_key: Key) -> Self {
Self {
offset_key,
_marker: Default::default(),
}
}
}

impl<K, V> Mapping<K, V>
where
K: PackedLayout,
V: PackedLayout,
{
/// Insert the given `value` to the contract storage.
#[inline]
pub fn insert<Q, R>(&mut self, key: Q, value: &R)
where
Q: scale::EncodeLike<K>,
R: scale::EncodeLike<V> + PackedLayout,
{
push_packed_root(value, &self.storage_key(key));
}

/// Get the `value` at `key` from the contract storage.
///
/// Returns `None` if no `value` exists at the given `key`.
#[inline]
pub fn get<Q>(&self, key: Q) -> Option<V>
where
Q: scale::EncodeLike<K>,
{
pull_packed_root_opt(&self.storage_key(key))
}

/// Returns a `Key` pointer used internally by the storage API.
///
/// This key is a combination of the `Mapping`'s internal `offset_key`
/// and the user provided `key`.
fn storage_key<Q>(&self, key: Q) -> Key
where
Q: scale::EncodeLike<K>,
{
let encodedable_key = (&self.offset_key, key);
let mut output = <Blake2x256 as HashOutput>::Type::default();
ink_env::hash_encoded::<Blake2x256, _>(&encodedable_key, &mut output);
output.into()
}
}

impl<K, V> SpreadLayout for Mapping<K, V> {
const FOOTPRINT: u64 = 1;
const REQUIRES_DEEP_CLEAN_UP: bool = false;

#[inline]
fn pull_spread(ptr: &mut KeyPtr) -> Self {
// Note: There is no need to pull anything from the storage for the
// mapping type since it initializes itself entirely by the
// given key pointer.
Self::new(*ExtKeyPtr::next_for::<Self>(ptr))
}

#[inline]
fn push_spread(&self, ptr: &mut KeyPtr) {
// Note: The mapping type does not store any state in its associated
// storage region, therefore only the pointer has to be incremented.
ptr.advance_by(Self::FOOTPRINT);
}

#[inline]
fn clear_spread(&self, ptr: &mut KeyPtr) {
// Note: The mapping type is not aware of its elements, therefore
// it is not possible to clean up after itself.
ptr.advance_by(Self::FOOTPRINT);
}
}

impl<K, V> SpreadAllocate for Mapping<K, V> {
#[inline]
fn allocate_spread(ptr: &mut KeyPtr) -> Self {
// Note: The mapping type initializes itself entirely by the key pointer.
Self::new(*ExtKeyPtr::next_for::<Self>(ptr))
}
}

#[cfg(feature = "std")]
const _: () = {
use crate::traits::StorageLayout;
use ink_metadata::layout::{
CellLayout,
Layout,
LayoutKey,
};

impl<K, V> StorageLayout for Mapping<K, V>
where
K: scale_info::TypeInfo + 'static,
V: scale_info::TypeInfo + 'static,
{
fn layout(key_ptr: &mut KeyPtr) -> Layout {
Layout::Cell(CellLayout::new::<Self>(LayoutKey::from(
key_ptr.advance_by(1),
)))
}
}
};

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn insert_and_get_work() {
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
let mut mapping: Mapping<u8, _> = Mapping::new([0u8; 32].into());
mapping.insert(&1, &2);
assert_eq!(mapping.get(&1), Some(2));

Ok(())
})
.unwrap()
}

#[test]
fn gets_default_if_no_key_set() {
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
let mapping: Mapping<u8, u8> = Mapping::new([0u8; 32].into());
assert_eq!(mapping.get(&1), None);

Ok(())
})
.unwrap()
}
}
2 changes: 2 additions & 0 deletions crates/storage/src/lazy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
//! extra care has to be taken when operating directly on them.

pub mod lazy_hmap;
pub mod mapping;

mod cache_cell;
mod entry;
Expand All @@ -46,6 +47,7 @@ pub use self::{
lazy_cell::LazyCell,
lazy_hmap::LazyHashMap,
lazy_imap::LazyIndexMap,
mapping::Mapping,
};
use crate::traits::{
KeyPtr,
Expand Down
24 changes: 13 additions & 11 deletions crates/storage/src/traits/optspec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,19 @@ pub fn pull_packed_root_opt<T>(root_key: &Key) -> Option<T>
where
T: PackedLayout,
{
match ink_env::get_contract_storage::<T>(root_key)
.expect("decoding does not match expected type")
{
Some(mut value) => {
// In case the contract storage is occupied we handle
// the Option<T> as if it was a T.
ink_env::get_contract_storage::<T>(root_key)
.unwrap_or_else(|error| {
panic!(
"failed to pull packed from root key {}: {:?}",
root_key, error
)
})
.map(|mut value| {
// In case the contract storage is occupied at the root key
// we handle the Option<T> as if it was a T.
<T as PackedLayout>::pull_packed(&mut value, root_key);
Some(value)
}
None => None,
}
value
})
}

pub fn push_packed_root_opt<T>(entity: Option<&T>, root_key: &Key)
Expand All @@ -128,7 +130,7 @@ where
super::push_packed_root(value, root_key)
}
None => {
// Clear the associated storage cell.
// Clear the associated storage cell since the entity is `None`.
ink_env::clear_contract_storage(root_key);
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/storage/src/traits/spread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ pub trait SpreadLayout {
///
/// # Examples
///
/// An instance of type `i32` requires one storage cell, so its footprint is
/// 1. An instance of type `(i32, i32)` requires 2 storage cells since a
/// An instance of type `i32` requires one storage cell, so its footprint is 1.
/// An instance of type `(i32, i32)` requires 2 storage cells since a
/// tuple or any other combined data structure always associates disjunctive
/// cells for its sub types. The same applies to arrays, e.g. `[i32; 5]`
/// has a footprint of 5.
Expand Down
Loading

0 comments on commit 7432565

Please sign in to comment.