Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
70172ac
Add `Mapping` storage collection
HCastano Sep 29, 2021
0ce829f
Implement `insert` and `get` for `Mapping`
HCastano Sep 29, 2021
e7e7f5d
Implement `SpreadLayout` for `Mapping`
HCastano Sep 29, 2021
1fead2f
Fix typo
HCastano Sep 29, 2021
62a6f01
Add some basic tests
HCastano Sep 29, 2021
f25b560
Fix some documentation formatting
HCastano Sep 29, 2021
9622ad5
Use `PackedLayout` as trait bound instead of `Encode/Decode`
HCastano Sep 30, 2021
73d4ab9
Avoid using low level `ink_env` functions when interacting with storage
HCastano Sep 30, 2021
92a4ce6
RustFmt
HCastano Sep 30, 2021
e1650c2
Appease Clippy
HCastano Sep 30, 2021
60c3370
Only use single `PhantomData` field
HCastano Sep 30, 2021
cfd4b03
Change `get` API to take reference to `key`
HCastano Sep 30, 2021
60bd5cd
Implement `TypeInfo` and `StorageLayout` for `Mapping`
HCastano Sep 30, 2021
e173a4d
Properly gate `TypeInfo` and `StorageLayout` impls behind `std`
HCastano Sep 30, 2021
99cad62
Replace `HashMap` with `Mapping` in ERC-20 example
HCastano Sep 30, 2021
047ae35
Return `Option` from `Mapping::get`
HCastano Oct 1, 2021
b880b5a
Update ERC-20 to handle `Option` returns
HCastano Oct 1, 2021
20395ab
Change `get` and `key` to use `Borrow`-ed values
HCastano Oct 11, 2021
955896b
Add `Debug` and `Default` implementations
HCastano Oct 11, 2021
de7e67f
Proper spelling
HCastano Oct 11, 2021
623ba34
Change `insert` to only accept borrowed K,V pairs
HCastano Oct 13, 2021
9d12813
Update ERC-20 example accordingly
HCastano Oct 13, 2021
387b752
Make more explicit what each `key` is referring to
HCastano Oct 13, 2021
5ce455c
Try using a `RefCell` instead of passing `Key` around
HCastano Oct 13, 2021
e272f3d
Try using `UnsafeCell` instead
HCastano Oct 13, 2021
b0ecb48
Revert "Try using a `RefCell` instead of passing `Key` around"
HCastano Oct 14, 2021
ebfaea5
Clean up some of the documentation
HCastano Oct 19, 2021
e652165
adjust the Mapping type for the new SpreadAllocate trait
Robbepop Oct 23, 2021
91f65f1
adjust ERC-20 example for changes in Mapping type
Robbepop Oct 23, 2021
8ee0882
remove commented out code
Robbepop Oct 23, 2021
8fcdbdf
add doc comment to new_init
Robbepop Oct 23, 2021
89f5799
make it possible to use references in more cases with Mapping
Robbepop Oct 23, 2021
53e63b4
use references in more cases for ERC-20 example contract
Robbepop Oct 23, 2021
cf823d1
remove unnecessary references in Mapping methods
Robbepop Oct 23, 2021
674a04d
refactor/improve pull_packed_root_opt utility method slightly
Robbepop Oct 23, 2021
46286d4
fix ERC-20 example contract
Robbepop Oct 23, 2021
8da218f
Merge branch 'master' into hc-rf-mapping-type
HCastano Nov 4, 2021
9ee7ee6
Merge branch 'hc-simple-mapping-primitive' into hc-rf-mapping-type
HCastano Nov 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions crates/storage/src/collections/mapping/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// 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>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can I think of EncodeLike sort-of like EncodeLike = Borrow + Encode?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

EncodeLike is a purely marker trait coming from the parity-scale-codec crate. Please go to the crates docs to read more about its general use.

Copy link
Contributor

Choose a reason for hiding this comment

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

I read the docs, and I think my question is still valid 😄

I think the semantics of a type being able to be SCALE encoded as another type (EncodeLike) vs. being able to be borrowed as another type (Borrow) are slightly different, which is why I asked

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
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
fn storage_key<Q>(&self, key: Q) -> Key
#[inline]
fn storage_key<Q>(&self, key: Q) -> 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.

have you benchmarked that this yields an improvement?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we don't need to specify the inline attribute. Because the compiler will decide it based on the optimization flag(in our case it reduces the size of the code). So compiler better knows what better to inline to reduce the size of the contract=)
https://nnethercote.github.io/perf-book/inlining.html

Copy link
Contributor

Choose a reason for hiding this comment

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

Just tried now, and there are no changes to the Wasm size. However, there are also no changes if I remove the inline's from insert() and get(), so maybe we should get rid of those as well?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if the compiler also takes into account that passing large types (> 64bit) is very expensive in wasm. Ideally, Q would be a reference.

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/collections/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod binary_heap;
pub mod bitstash;
pub mod bitvec;
pub mod hashmap;
pub mod mapping;
pub mod smallvec;
pub mod stash;
pub mod vec;
Expand All @@ -32,6 +33,7 @@ pub use self::{
bitstash::BitStash,
bitvec::Bitvec,
hashmap::HashMap,
mapping::Mapping,
stash::Stash,
vec::Vec,
};
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