Skip to content

Commit 7432565

Browse files
HCastanoRobbepop
andauthored
Simple Mapping Storage Primitive (#946)
* 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]>
1 parent ec7d5e8 commit 7432565

File tree

5 files changed

+267
-41
lines changed

5 files changed

+267
-41
lines changed

crates/storage/src/lazy/mapping.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Copyright 2018-2021 Parity Technologies (UK) Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! A simple mapping to contract storage.
16+
//!
17+
//! # Note
18+
//!
19+
//! This mapping doesn't actually "own" any data.
20+
//! Instead it is just a simple wrapper around the contract storage facilities.
21+
22+
use crate::traits::{
23+
pull_packed_root_opt,
24+
push_packed_root,
25+
ExtKeyPtr,
26+
KeyPtr,
27+
PackedLayout,
28+
SpreadAllocate,
29+
SpreadLayout,
30+
};
31+
use core::marker::PhantomData;
32+
33+
use ink_env::hash::{
34+
Blake2x256,
35+
HashOutput,
36+
};
37+
use ink_primitives::Key;
38+
39+
/// A mapping of key-value pairs directly into contract storage.
40+
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
41+
#[derive(Default)]
42+
pub struct Mapping<K, V> {
43+
offset_key: Key,
44+
_marker: PhantomData<fn() -> (K, V)>,
45+
}
46+
47+
impl<K, V> core::fmt::Debug for Mapping<K, V> {
48+
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
49+
f.debug_struct("Mapping")
50+
.field("offset_key", &self.offset_key)
51+
.finish()
52+
}
53+
}
54+
55+
impl<K, V> Mapping<K, V> {
56+
/// Creates a new empty `Mapping`.
57+
fn new(offset_key: Key) -> Self {
58+
Self {
59+
offset_key,
60+
_marker: Default::default(),
61+
}
62+
}
63+
}
64+
65+
impl<K, V> Mapping<K, V>
66+
where
67+
K: PackedLayout,
68+
V: PackedLayout,
69+
{
70+
/// Insert the given `value` to the contract storage.
71+
#[inline]
72+
pub fn insert<Q, R>(&mut self, key: Q, value: &R)
73+
where
74+
Q: scale::EncodeLike<K>,
75+
R: scale::EncodeLike<V> + PackedLayout,
76+
{
77+
push_packed_root(value, &self.storage_key(key));
78+
}
79+
80+
/// Get the `value` at `key` from the contract storage.
81+
///
82+
/// Returns `None` if no `value` exists at the given `key`.
83+
#[inline]
84+
pub fn get<Q>(&self, key: Q) -> Option<V>
85+
where
86+
Q: scale::EncodeLike<K>,
87+
{
88+
pull_packed_root_opt(&self.storage_key(key))
89+
}
90+
91+
/// Returns a `Key` pointer used internally by the storage API.
92+
///
93+
/// This key is a combination of the `Mapping`'s internal `offset_key`
94+
/// and the user provided `key`.
95+
fn storage_key<Q>(&self, key: Q) -> Key
96+
where
97+
Q: scale::EncodeLike<K>,
98+
{
99+
let encodedable_key = (&self.offset_key, key);
100+
let mut output = <Blake2x256 as HashOutput>::Type::default();
101+
ink_env::hash_encoded::<Blake2x256, _>(&encodedable_key, &mut output);
102+
output.into()
103+
}
104+
}
105+
106+
impl<K, V> SpreadLayout for Mapping<K, V> {
107+
const FOOTPRINT: u64 = 1;
108+
const REQUIRES_DEEP_CLEAN_UP: bool = false;
109+
110+
#[inline]
111+
fn pull_spread(ptr: &mut KeyPtr) -> Self {
112+
// Note: There is no need to pull anything from the storage for the
113+
// mapping type since it initializes itself entirely by the
114+
// given key pointer.
115+
Self::new(*ExtKeyPtr::next_for::<Self>(ptr))
116+
}
117+
118+
#[inline]
119+
fn push_spread(&self, ptr: &mut KeyPtr) {
120+
// Note: The mapping type does not store any state in its associated
121+
// storage region, therefore only the pointer has to be incremented.
122+
ptr.advance_by(Self::FOOTPRINT);
123+
}
124+
125+
#[inline]
126+
fn clear_spread(&self, ptr: &mut KeyPtr) {
127+
// Note: The mapping type is not aware of its elements, therefore
128+
// it is not possible to clean up after itself.
129+
ptr.advance_by(Self::FOOTPRINT);
130+
}
131+
}
132+
133+
impl<K, V> SpreadAllocate for Mapping<K, V> {
134+
#[inline]
135+
fn allocate_spread(ptr: &mut KeyPtr) -> Self {
136+
// Note: The mapping type initializes itself entirely by the key pointer.
137+
Self::new(*ExtKeyPtr::next_for::<Self>(ptr))
138+
}
139+
}
140+
141+
#[cfg(feature = "std")]
142+
const _: () = {
143+
use crate::traits::StorageLayout;
144+
use ink_metadata::layout::{
145+
CellLayout,
146+
Layout,
147+
LayoutKey,
148+
};
149+
150+
impl<K, V> StorageLayout for Mapping<K, V>
151+
where
152+
K: scale_info::TypeInfo + 'static,
153+
V: scale_info::TypeInfo + 'static,
154+
{
155+
fn layout(key_ptr: &mut KeyPtr) -> Layout {
156+
Layout::Cell(CellLayout::new::<Self>(LayoutKey::from(
157+
key_ptr.advance_by(1),
158+
)))
159+
}
160+
}
161+
};
162+
163+
#[cfg(test)]
164+
mod tests {
165+
use super::*;
166+
167+
#[test]
168+
fn insert_and_get_work() {
169+
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
170+
let mut mapping: Mapping<u8, _> = Mapping::new([0u8; 32].into());
171+
mapping.insert(&1, &2);
172+
assert_eq!(mapping.get(&1), Some(2));
173+
174+
Ok(())
175+
})
176+
.unwrap()
177+
}
178+
179+
#[test]
180+
fn gets_default_if_no_key_set() {
181+
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
182+
let mapping: Mapping<u8, u8> = Mapping::new([0u8; 32].into());
183+
assert_eq!(mapping.get(&1), None);
184+
185+
Ok(())
186+
})
187+
.unwrap()
188+
}
189+
}

crates/storage/src/lazy/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
//! extra care has to be taken when operating directly on them.
2626
2727
pub mod lazy_hmap;
28+
pub mod mapping;
2829

2930
mod cache_cell;
3031
mod entry;
@@ -46,6 +47,7 @@ pub use self::{
4647
lazy_cell::LazyCell,
4748
lazy_hmap::LazyHashMap,
4849
lazy_imap::LazyIndexMap,
50+
mapping::Mapping,
4951
};
5052
use crate::traits::{
5153
KeyPtr,

crates/storage/src/traits/optspec.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,19 @@ pub fn pull_packed_root_opt<T>(root_key: &Key) -> Option<T>
102102
where
103103
T: PackedLayout,
104104
{
105-
match ink_env::get_contract_storage::<T>(root_key)
106-
.expect("decoding does not match expected type")
107-
{
108-
Some(mut value) => {
109-
// In case the contract storage is occupied we handle
110-
// the Option<T> as if it was a T.
105+
ink_env::get_contract_storage::<T>(root_key)
106+
.unwrap_or_else(|error| {
107+
panic!(
108+
"failed to pull packed from root key {}: {:?}",
109+
root_key, error
110+
)
111+
})
112+
.map(|mut value| {
113+
// In case the contract storage is occupied at the root key
114+
// we handle the Option<T> as if it was a T.
111115
<T as PackedLayout>::pull_packed(&mut value, root_key);
112-
Some(value)
113-
}
114-
None => None,
115-
}
116+
value
117+
})
116118
}
117119

118120
pub fn push_packed_root_opt<T>(entity: Option<&T>, root_key: &Key)
@@ -128,7 +130,7 @@ where
128130
super::push_packed_root(value, root_key)
129131
}
130132
None => {
131-
// Clear the associated storage cell.
133+
// Clear the associated storage cell since the entity is `None`.
132134
ink_env::clear_contract_storage(root_key);
133135
}
134136
}

crates/storage/src/traits/spread.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ pub trait SpreadLayout {
4343
///
4444
/// # Examples
4545
///
46-
/// An instance of type `i32` requires one storage cell, so its footprint is
47-
/// 1. An instance of type `(i32, i32)` requires 2 storage cells since a
46+
/// An instance of type `i32` requires one storage cell, so its footprint is 1.
47+
/// An instance of type `(i32, i32)` requires 2 storage cells since a
4848
/// tuple or any other combined data structure always associates disjunctive
4949
/// cells for its sub types. The same applies to arrays, e.g. `[i32; 5]`
5050
/// has a footprint of 5.

0 commit comments

Comments
 (0)