Add additionally functionality to contracts storage interface (#10497)

* Add new versions for storage access host functions

* Improve docs
This commit is contained in:
Alexander Theißen
2022-01-08 13:19:23 +01:00
committed by GitHub
parent 4b9818a06a
commit 46e38e8288
8 changed files with 1394 additions and 602 deletions
+303 -11
View File
@@ -257,21 +257,22 @@ mod tests {
AccountIdOf, BlockNumberOf, ErrorOrigin, ExecError, Executable, Ext, SeedOf, StorageKey,
},
gas::GasMeter,
storage::WriteOutcome,
tests::{Call, Test, ALICE, BOB},
BalanceOf, CodeHash, Error, Pallet as Contracts,
};
use assert_matches::assert_matches;
use frame_support::{
assert_ok,
dispatch::{DispatchResult, DispatchResultWithPostInfo},
weights::Weight,
};
use frame_support::{assert_ok, dispatch::DispatchResultWithPostInfo, weights::Weight};
use hex_literal::hex;
use pallet_contracts_primitives::{ExecReturnValue, ReturnFlags};
use pretty_assertions::assert_eq;
use sp_core::{Bytes, H256};
use sp_runtime::DispatchError;
use std::{borrow::BorrowMut, cell::RefCell, collections::HashMap};
use std::{
borrow::BorrowMut,
cell::RefCell,
collections::hash_map::{Entry, HashMap},
};
#[derive(Debug, PartialEq, Eq)]
struct InstantiateEntry {
@@ -384,9 +385,26 @@ mod tests {
fn get_storage(&mut self, key: &StorageKey) -> Option<Vec<u8>> {
self.storage.get(key).cloned()
}
fn set_storage(&mut self, key: StorageKey, value: Option<Vec<u8>>) -> DispatchResult {
*self.storage.entry(key).or_insert(Vec::new()) = value.unwrap_or(Vec::new());
Ok(())
fn contains_storage(&mut self, key: &StorageKey) -> bool {
self.storage.contains_key(key)
}
fn set_storage(
&mut self,
key: StorageKey,
value: Option<Vec<u8>>,
take_old: bool,
) -> Result<WriteOutcome, DispatchError> {
let entry = self.storage.entry(key);
let result = match (entry, take_old) {
(Entry::Vacant(_), _) => WriteOutcome::New,
(Entry::Occupied(entry), false) =>
WriteOutcome::Overwritten(entry.remove().len() as u32),
(Entry::Occupied(entry), true) => WriteOutcome::Taken(entry.remove()),
};
if let Some(value) = value {
self.storage.insert(key, value);
}
Ok(result)
}
fn caller(&self) -> &AccountIdOf<Self::T> {
&ALICE
@@ -932,8 +950,8 @@ mod tests {
"\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11"
)
;; [32, 36) buffer size = 128 bytes
(data (i32.const 32) "\80")
;; [32, 36) buffer size = 4k in little endian
(data (i32.const 32) "\00\10")
;; [36; inf) buffer where the result is copied
@@ -1957,4 +1975,278 @@ mod tests {
);
assert_eq!(*ext.runtime_calls.borrow(), vec![]);
}
#[test]
#[cfg(feature = "unstable-interface")]
fn set_storage_works() {
const CODE: &str = r#"
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "__unstable__" "seal_set_storage" (func $seal_set_storage (param i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1))
;; 0x1000 = 4k in little endian
;; size of input buffer
(data (i32.const 0) "\00\10")
(func (export "call")
;; Receive (key ++ value_to_write)
(call $seal_input
(i32.const 4) ;; Pointer to the input buffer
(i32.const 0) ;; Size of the length buffer
)
;; Store the passed value to the passed key and store result to memory
(i32.store (i32.const 0)
(call $seal_set_storage
(i32.const 4) ;; key_ptr
(i32.const 36) ;; value_ptr
(i32.sub ;; value_len (input_size - key_size)
(i32.load (i32.const 0))
(i32.const 32)
)
)
)
(call $seal_return
(i32.const 0) ;; flags
(i32.const 0) ;; returned value
(i32.const 4) ;; length of returned value
)
)
(func (export "deploy"))
)
"#;
let mut ext = MockExt::default();
// value did not exist before -> sentinel returned
let input = ([1u8; 32], [42u8, 48]).encode();
let result = execute(CODE, input, &mut ext).unwrap();
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), u32::MAX);
assert_eq!(ext.storage.get(&[1u8; 32]).unwrap(), &[42u8, 48]);
// value do exist -> length of old value returned
let input = ([1u8; 32], [0u8; 0]).encode();
let result = execute(CODE, input, &mut ext).unwrap();
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 2);
assert_eq!(ext.storage.get(&[1u8; 32]).unwrap(), &[0u8; 0]);
// value do exist -> length of old value returned (test for zero sized val)
let input = ([1u8; 32], [99u8]).encode();
let result = execute(CODE, input, &mut ext).unwrap();
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 0);
assert_eq!(ext.storage.get(&[1u8; 32]).unwrap(), &[99u8]);
}
#[test]
#[cfg(feature = "unstable-interface")]
fn clear_storage_works() {
const CODE: &str = r#"
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "__unstable__" "seal_clear_storage" (func $seal_clear_storage (param i32) (result i32)))
(import "env" "memory" (memory 1 1))
;; 0x1000 = 4k in little endian
;; size of input buffer
(data (i32.const 0) "\00\10")
(func (export "call")
;; Receive key
(call $seal_input
(i32.const 4) ;; Pointer to the input buffer
(i32.const 0) ;; Size of the length buffer
)
;; Store the passed value to the passed key and store result to memory
(i32.store (i32.const 0)
(call $seal_clear_storage
(i32.const 4) ;; key_ptr
)
)
(call $seal_return
(i32.const 0) ;; flags
(i32.const 0) ;; returned value
(i32.const 4) ;; length of returned value
)
)
(func (export "deploy"))
)
"#;
let mut ext = MockExt::default();
ext.storage.insert([1u8; 32], vec![42u8]);
ext.storage.insert([2u8; 32], vec![]);
// value does not exist -> sentinel returned
let result = execute(CODE, [3u8; 32].encode(), &mut ext).unwrap();
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), u32::MAX);
assert_eq!(ext.storage.get(&[3u8; 32]), None);
// value did exist -> length returned
let result = execute(CODE, [1u8; 32].encode(), &mut ext).unwrap();
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 1);
assert_eq!(ext.storage.get(&[1u8; 32]), None);
// value did exist -> length returned (test for 0 sized)
let result = execute(CODE, [2u8; 32].encode(), &mut ext).unwrap();
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 0);
assert_eq!(ext.storage.get(&[2u8; 32]), None);
}
#[test]
#[cfg(feature = "unstable-interface")]
fn take_storage_works() {
const CODE: &str = r#"
(module
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "__unstable__" "seal_take_storage" (func $seal_take_storage (param i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1))
;; [0, 32) size of input buffer (32 byte as we copy the key here)
(data (i32.const 0) "\20")
;; [32, 64) size of output buffer
;; 4k in little endian
(data (i32.const 32) "\00\10")
;; [64, 96) input buffer
;; [96, inf) output buffer
(func (export "call")
;; Receive key
(call $seal_input
(i32.const 64) ;; Pointer to the input buffer
(i32.const 0) ;; Size of the length buffer
)
;; Load a storage value and result of this call into the output buffer
(i32.store (i32.const 96)
(call $seal_take_storage
(i32.const 64) ;; The pointer to the storage key to fetch
(i32.const 100) ;; Pointer to the output buffer
(i32.const 32) ;; Pointer to the size of the buffer
)
)
;; Return the contents of the buffer
(call $seal_return
(i32.const 0) ;; flags
(i32.const 96) ;; output buffer ptr
(i32.add ;; length: storage size + 4 (retval)
(i32.load (i32.const 32))
(i32.const 4)
)
)
)
(func (export "deploy"))
)
"#;
let mut ext = MockExt::default();
ext.storage.insert([1u8; 32], vec![42u8]);
ext.storage.insert([2u8; 32], vec![]);
// value does not exist -> error returned
let result = execute(CODE, [3u8; 32].encode(), &mut ext).unwrap();
assert_eq!(
u32::from_le_bytes(result.data.0[0..4].try_into().unwrap()),
ReturnCode::KeyNotFound as u32
);
// value did exist -> value returned
let result = execute(CODE, [1u8; 32].encode(), &mut ext).unwrap();
assert_eq!(
u32::from_le_bytes(result.data.0[0..4].try_into().unwrap()),
ReturnCode::Success as u32
);
assert_eq!(ext.storage.get(&[1u8; 32]), None);
assert_eq!(&result.data.0[4..], &[42u8]);
// value did exist -> length returned (test for 0 sized)
let result = execute(CODE, [2u8; 32].encode(), &mut ext).unwrap();
assert_eq!(
u32::from_le_bytes(result.data.0[0..4].try_into().unwrap()),
ReturnCode::Success as u32
);
assert_eq!(ext.storage.get(&[2u8; 32]), None);
assert_eq!(&result.data.0[4..], &[0u8; 0]);
}
#[test]
#[cfg(feature = "unstable-interface")]
fn contains_storage_works() {
const CODE: &str = r#"
(module
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "__unstable__" "seal_contains_storage" (func $seal_contains_storage (param i32) (result i32)))
(import "env" "memory" (memory 1 1))
;; [0, 4) size of input buffer (32 byte as we copy the key here)
(data (i32.const 0) "\20")
;; [4, 36) input buffer
;; [36, inf) output buffer
(func (export "call")
;; Receive key
(call $seal_input
(i32.const 4) ;; Pointer to the input buffer
(i32.const 0) ;; Size of the length buffer
)
;; Load the return value into the output buffer
(i32.store (i32.const 36)
(call $seal_contains_storage
(i32.const 4) ;; The pointer to the storage key to fetch
)
)
;; Return the contents of the buffer
(call $seal_return
(i32.const 0) ;; flags
(i32.const 36) ;; output buffer ptr
(i32.const 4) ;; result is integer (4 bytes)
)
)
(func (export "deploy"))
)
"#;
let mut ext = MockExt::default();
ext.storage.insert([1u8; 32], vec![42u8]);
ext.storage.insert([2u8; 32], vec![]);
// value does not exist -> error returned
let result = execute(CODE, [3u8; 32].encode(), &mut ext).unwrap();
assert_eq!(
u32::from_le_bytes(result.data.0.try_into().unwrap()),
ReturnCode::KeyNotFound as u32
);
// value did exist -> success
let result = execute(CODE, [1u8; 32].encode(), &mut ext).unwrap();
assert_eq!(
u32::from_le_bytes(result.data.0.try_into().unwrap()),
ReturnCode::Success as u32
);
// value did exist -> success (zero sized type)
let result = execute(CODE, [2u8; 32].encode(), &mut ext).unwrap();
assert_eq!(
u32::from_le_bytes(result.data.0.try_into().unwrap()),
ReturnCode::Success as u32
);
}
}
+138 -18
View File
@@ -21,6 +21,7 @@ use crate::{
exec::{ExecError, ExecResult, Ext, StorageKey, TopicOf},
gas::{ChargedAmount, Token},
schedule::HostFnWeights,
storage::WriteOutcome,
wasm::env_def::ConvertibleToWasm,
BalanceOf, CodeHash, Config, Error,
};
@@ -172,10 +173,19 @@ pub enum RuntimeCosts {
SetStorage(u32),
/// Weight of calling `seal_clear_storage`.
ClearStorage,
/// Weight of calling `seal_contains_storage`.
#[cfg(feature = "unstable-interface")]
ContainsStorage,
/// Weight of calling `seal_get_storage` without output weight.
GetStorageBase,
/// Weight of an item received via `seal_get_storage` for the given size.
GetStorageCopyOut(u32),
/// Weight of calling `seal_take_storage` without output weight.
#[cfg(feature = "unstable-interface")]
TakeStorageBase,
/// Weight of an item received via `seal_take_storage` for the given size.
#[cfg(feature = "unstable-interface")]
TakeStorageCopyOut(u32),
/// Weight of calling `seal_transfer`.
Transfer,
/// Weight of calling `seal_call` for the given input size.
@@ -242,8 +252,14 @@ impl RuntimeCosts {
SetStorage(len) =>
s.set_storage.saturating_add(s.set_storage_per_byte.saturating_mul(len.into())),
ClearStorage => s.clear_storage,
#[cfg(feature = "unstable-interface")]
ContainsStorage => s.contains_storage,
GetStorageBase => s.get_storage,
GetStorageCopyOut(len) => s.get_storage_per_byte.saturating_mul(len.into()),
#[cfg(feature = "unstable-interface")]
TakeStorageBase => s.take_storage,
#[cfg(feature = "unstable-interface")]
TakeStorageCopyOut(len) => s.take_storage_per_byte.saturating_mul(len.into()),
Transfer => s.transfer,
CallBase(len) =>
s.call.saturating_add(s.call_per_input_byte.saturating_mul(len.into())),
@@ -632,6 +648,50 @@ where
}
}
/// Extracts the size of the overwritten value or `u32::MAX` if there
/// was no value in storage.
///
/// # Note
///
/// We cannot use `0` as sentinel value because there could be a zero sized
/// storage entry which is different from a non existing one.
fn overwritten_len(outcome: WriteOutcome) -> u32 {
match outcome {
WriteOutcome::New => u32::MAX,
WriteOutcome::Overwritten(len) => len,
WriteOutcome::Taken(value) => value.len() as u32,
}
}
fn set_storage(
&mut self,
key_ptr: u32,
value_ptr: u32,
value_len: u32,
) -> Result<u32, TrapReason> {
self.charge_gas(RuntimeCosts::SetStorage(value_len))?;
if value_len > self.ext.max_value_size() {
Err(Error::<E::T>::ValueTooLarge)?;
}
let mut key: StorageKey = [0; 32];
self.read_sandbox_memory_into_buf(key_ptr, &mut key)?;
let value = Some(self.read_sandbox_memory(value_ptr, value_len)?);
self.ext
.set_storage(key, value, false)
.map(Self::overwritten_len)
.map_err(Into::into)
}
fn clear_storage(&mut self, key_ptr: u32) -> Result<u32, TrapReason> {
self.charge_gas(RuntimeCosts::ClearStorage)?;
let mut key: StorageKey = [0; 32];
self.read_sandbox_memory_into_buf(key_ptr, &mut key)?;
self.ext
.set_storage(key, None, false)
.map(Self::overwritten_len)
.map_err(Into::into)
}
fn call(
&mut self,
flags: CallFlags,
@@ -745,10 +805,18 @@ define_env!(Env, <E: Ext>,
Ok(())
},
// Set the value at the given key in the contract storage.
//
// Equivalent to the newer version of `seal_set_storage` with the exception of the return
// type. Still a valid thing to call when not interested in the return value.
[seal0] seal_set_storage(ctx, key_ptr: u32, value_ptr: u32, value_len: u32) => {
ctx.set_storage(key_ptr, value_ptr, value_len).map(|_| ())
},
// Set the value at the given key in the contract storage.
//
// The value length must not exceed the maximum defined by the contracts module parameters.
// Storing an empty value is disallowed.
// Specifying a `value_len` of zero will store an empty value.
//
// # Parameters
//
@@ -756,19 +824,20 @@ define_env!(Env, <E: Ext>,
// - `value_ptr`: pointer into the linear memory where the value to set is placed.
// - `value_len`: the length of the value in bytes.
//
// # Traps
// # Return Value
//
// - If value length exceeds the configured maximum value length of a storage entry.
// - Upon trying to set an empty storage entry (value length is 0).
[seal0] seal_set_storage(ctx, key_ptr: u32, value_ptr: u32, value_len: u32) => {
ctx.charge_gas(RuntimeCosts::SetStorage(value_len))?;
if value_len > ctx.ext.max_value_size() {
Err(Error::<E::T>::ValueTooLarge)?;
}
let mut key: StorageKey = [0; 32];
ctx.read_sandbox_memory_into_buf(key_ptr, &mut key)?;
let value = Some(ctx.read_sandbox_memory(value_ptr, value_len)?);
ctx.ext.set_storage(key, value).map_err(Into::into)
// Returns the size of the pre-existing value at the specified key if any. Otherwise
// `u32::MAX` is returned as a sentinel value.
[__unstable__] seal_set_storage(ctx, key_ptr: u32, value_ptr: u32, value_len: u32) -> u32 => {
ctx.set_storage(key_ptr, value_ptr, value_len)
},
// Clear the value at the given key in the contract storage.
//
// Equivalent to the newer version of `seal_clear_storage` with the exception of the return
// type. Still a valid thing to call when not interested in the return value.
[seal0] seal_clear_storage(ctx, key_ptr: u32) => {
ctx.clear_storage(key_ptr).map(|_| ()).map_err(Into::into)
},
// Clear the value at the given key in the contract storage.
@@ -776,11 +845,13 @@ define_env!(Env, <E: Ext>,
// # Parameters
//
// - `key_ptr`: pointer into the linear memory where the location to clear the value is placed.
[seal0] seal_clear_storage(ctx, key_ptr: u32) => {
ctx.charge_gas(RuntimeCosts::ClearStorage)?;
let mut key: StorageKey = [0; 32];
ctx.read_sandbox_memory_into_buf(key_ptr, &mut key)?;
ctx.ext.set_storage(key, None).map_err(Into::into)
//
// # Return Value
//
// Returns the size of the pre-existing value at the specified key if any. Otherwise
// `u32::MAX` is returned as a sentinel value.
[__unstable__] seal_clear_storage(ctx, key_ptr: u32) -> u32 => {
ctx.clear_storage(key_ptr).map_err(Into::into)
},
// Retrieve the value under the given key from storage.
@@ -809,6 +880,55 @@ define_env!(Env, <E: Ext>,
}
},
// Checks whether there is a value stored under the given key.
//
// Returns `ReturnCode::Success` if there is a key in storage. Otherwise an error
// is returned.
//
// # Parameters
//
// - `key_ptr`: pointer into the linear memory where the key of the requested value is placed.
//
// # Errors
//
// `ReturnCode::KeyNotFound`
[__unstable__] seal_contains_storage(ctx, key_ptr: u32) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::ContainsStorage)?;
let mut key: StorageKey = [0; 32];
ctx.read_sandbox_memory_into_buf(key_ptr, &mut key)?;
if ctx.ext.contains_storage(&key) {
Ok(ReturnCode::Success)
} else {
Ok(ReturnCode::KeyNotFound)
}
},
// Retrieve and remove the value under the given key from storage.
//
// # Parameters
//
// - `key_ptr`: pointer into the linear memory where the key of the requested value is placed.
// - `out_ptr`: pointer to the linear memory where the value is written to.
// - `out_len_ptr`: in-out pointer into linear memory where the buffer length
// is read from and the value length is written to.
//
// # Errors
//
// `ReturnCode::KeyNotFound`
[__unstable__] seal_take_storage(ctx, key_ptr: u32, out_ptr: u32, out_len_ptr: u32) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::TakeStorageBase)?;
let mut key: StorageKey = [0; 32];
ctx.read_sandbox_memory_into_buf(key_ptr, &mut key)?;
if let WriteOutcome::Taken(value) = ctx.ext.set_storage(key, None, true)? {
ctx.write_sandbox_output(out_ptr, out_len_ptr, &value, false, |len| {
Some(RuntimeCosts::TakeStorageCopyOut(len))
})?;
Ok(ReturnCode::Success)
} else {
Ok(ReturnCode::KeyNotFound)
}
},
// Transfer some value to another account.
//
// # Parameters