Pre-Charge max size when contracts access storage (#10691)

* Fix seal_get_storage

* Fix seal_take_storage

* Add more benchmarks

* cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_contracts --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/contracts/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Fix seal_set_storage

* Fix seal_contains_storage and seal_clear_storage

* Fix benchmarks

* cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_contracts --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/contracts/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Get rid of mem::size_of in benchmarks

* Fix up code loading

* Apply suggestions from code review

Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>

* Fix test to call same function twice

* Replaced u32::MAX by SENTINEL const

* Fix seal_contains_storage benchmark

* cargo run --quiet --profile=production  --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_contracts --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/contracts/src/weights.rs --template=./.maintain/frame-weight-template.hbs

Co-authored-by: Parity Bot <admin@parity.io>
Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
This commit is contained in:
Alexander Theißen
2022-01-24 21:14:31 +01:00
committed by GitHub
parent e327b734bc
commit dc45201a64
11 changed files with 1141 additions and 926 deletions
@@ -38,7 +38,6 @@ use crate::{
use frame_support::{
dispatch::{DispatchError, DispatchResult},
ensure,
storage::StorageMap,
traits::ReservableCurrency,
};
use sp_core::crypto::UncheckedFrom;
@@ -149,52 +148,38 @@ pub fn load<T: Config>(
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
gas_meter.charge(CodeToken::Load(estimate_code_size::<T, CodeStorage<T>, _>(&code_hash)?))?;
let charged = gas_meter.charge(CodeToken::Load(schedule.limits.code_len))?;
let mut prefab_module =
<CodeStorage<T>>::get(code_hash).ok_or_else(|| Error::<T>::CodeNotFound)?;
gas_meter.adjust_gas(charged, CodeToken::Load(prefab_module.code.len() as u32));
prefab_module.code_hash = code_hash;
if prefab_module.instruction_weights_version < schedule.instruction_weights.version {
// The instruction weights have changed.
// We need to re-instrument the code with the new instruction weights.
gas_meter.charge(CodeToken::Reinstrument(estimate_code_size::<T, PristineCode<T>, _>(
&code_hash,
)?))?;
reinstrument(&mut prefab_module, schedule)?;
let charged = gas_meter.charge(CodeToken::Reinstrument(schedule.limits.code_len))?;
let code_size = reinstrument(&mut prefab_module, schedule)?;
gas_meter.adjust_gas(charged, CodeToken::Reinstrument(code_size));
}
Ok(prefab_module)
}
/// Instruments the passed prefab wasm module with the supplied schedule.
///
/// Returns the size in bytes of the uninstrumented code.
pub fn reinstrument<T: Config>(
prefab_module: &mut PrefabWasmModule<T>,
schedule: &Schedule<T>,
) -> Result<(), DispatchError> {
) -> Result<u32, DispatchError> {
let original_code =
<PristineCode<T>>::get(&prefab_module.code_hash).ok_or_else(|| Error::<T>::CodeNotFound)?;
let original_code_len = original_code.len();
prefab_module.code = prepare::reinstrument_contract::<T>(original_code, schedule)?;
prefab_module.instruction_weights_version = schedule.instruction_weights.version;
<CodeStorage<T>>::insert(&prefab_module.code_hash, &*prefab_module);
Ok(())
}
/// Get the size of the code stored at `code_hash` without loading it.
///
/// The returned value is slightly too large when using it for the [`PrefabWasmModule`]
/// because it has other fields in addition to the code itself. However, those are negligible
/// when compared to the code size. Additionally, charging too much weight is completely safe.
fn estimate_code_size<T, M, V>(code_hash: &CodeHash<T>) -> Result<u32, DispatchError>
where
T: Config,
M: StorageMap<CodeHash<T>, V>,
V: codec::FullCodec,
{
let key = M::hashed_key_for(code_hash);
let mut data = [0u8; 0];
let len = sp_io::storage::read(&key, &mut data, 0).ok_or_else(|| Error::<T>::CodeNotFound)?;
Ok(len)
Ok(original_code_len as u32)
}
/// Costs for operations that are related to code handling.
+8 -17
View File
@@ -385,8 +385,8 @@ mod tests {
fn get_storage(&mut self, key: &StorageKey) -> Option<Vec<u8>> {
self.storage.get(key).cloned()
}
fn contains_storage(&mut self, key: &StorageKey) -> bool {
self.storage.contains_key(key)
fn get_storage_size(&mut self, key: &StorageKey) -> Option<u32> {
self.storage.get(key).map(|val| val.len() as u32)
}
fn set_storage(
&mut self,
@@ -2023,7 +2023,7 @@ mod tests {
// 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!(u32::from_le_bytes(result.data.0.try_into().unwrap()), crate::SENTINEL);
assert_eq!(ext.storage.get(&[1u8; 32]).unwrap(), &[42u8, 48]);
// value do exist -> length of old value returned
@@ -2083,7 +2083,7 @@ mod tests {
// 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!(u32::from_le_bytes(result.data.0.try_into().unwrap()), crate::SENTINEL);
assert_eq!(ext.storage.get(&[3u8; 32]), None);
// value did exist -> length returned
@@ -2228,25 +2228,16 @@ mod tests {
ext.storage.insert([1u8; 32], vec![42u8]);
ext.storage.insert([2u8; 32], vec![]);
// value does not exist -> error returned
// value does not exist -> sentinel value 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
);
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), crate::SENTINEL);
// 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
);
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 1,);
// 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
);
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 0,);
}
}
+65 -77
View File
@@ -21,9 +21,8 @@ use crate::{
exec::{ExecError, ExecResult, Ext, StorageKey, TopicOf},
gas::{ChargedAmount, Token},
schedule::HostFnWeights,
storage::WriteOutcome,
wasm::env_def::ConvertibleToWasm,
BalanceOf, CodeHash, Config, Error,
BalanceOf, CodeHash, Config, Error, SENTINEL,
};
use bitflags::bitflags;
use codec::{Decode, DecodeAll, Encode, MaxEncodedLen};
@@ -169,23 +168,18 @@ pub enum RuntimeCosts {
DepositEvent { num_topic: u32, len: u32 },
/// Weight of calling `seal_debug_message`.
DebugMessage,
/// Weight of calling `seal_set_storage` for the given storage item size.
SetStorage(u32),
/// Weight of calling `seal_clear_storage`.
ClearStorage,
/// Weight of calling `seal_contains_storage`.
/// Weight of calling `seal_set_storage` for the given storage item sizes.
SetStorage { old_bytes: u32, new_bytes: u32 },
/// Weight of calling `seal_clear_storage` per cleared byte.
ClearStorage(u32),
/// Weight of calling `seal_contains_storage` per byte of the checked item.
#[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.
ContainsStorage(u32),
/// Weight of calling `seal_get_storage` with the specified size in storage.
GetStorage(u32),
/// Weight of calling `seal_take_storage` for the given size.
#[cfg(feature = "unstable-interface")]
TakeStorageBase,
/// Weight of an item received via `seal_take_storage` for the given size.
#[cfg(feature = "unstable-interface")]
TakeStorageCopyOut(u32),
TakeStorage(u32),
/// Weight of calling `seal_transfer`.
Transfer,
/// Weight of calling `seal_call` for the given input size.
@@ -249,17 +243,23 @@ impl RuntimeCosts {
.saturating_add(s.deposit_event_per_topic.saturating_mul(num_topic.into()))
.saturating_add(s.deposit_event_per_byte.saturating_mul(len.into())),
DebugMessage => s.debug_message,
SetStorage(len) =>
s.set_storage.saturating_add(s.set_storage_per_byte.saturating_mul(len.into())),
ClearStorage => s.clear_storage,
SetStorage { new_bytes, old_bytes } => s
.set_storage
.saturating_add(s.set_storage_per_new_byte.saturating_mul(new_bytes.into()))
.saturating_add(s.set_storage_per_old_byte.saturating_mul(old_bytes.into())),
ClearStorage(len) => s
.clear_storage
.saturating_add(s.clear_storage_per_byte.saturating_mul(len.into())),
#[cfg(feature = "unstable-interface")]
ContainsStorage => s.contains_storage,
GetStorageBase => s.get_storage,
GetStorageCopyOut(len) => s.get_storage_per_byte.saturating_mul(len.into()),
ContainsStorage(len) => s
.contains_storage
.saturating_add(s.contains_storage_per_byte.saturating_mul(len.into())),
GetStorage(len) =>
s.get_storage.saturating_add(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()),
TakeStorage(len) => s
.take_storage
.saturating_add(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())),
@@ -534,7 +534,7 @@ where
/// length of the buffer located at `out_ptr`. If that buffer is large enough the actual
/// `buf.len()` is written to this location.
///
/// If `out_ptr` is set to the sentinel value of `u32::MAX` and `allow_skip` is true the
/// If `out_ptr` is set to the sentinel value of `SENTINEL` and `allow_skip` is true the
/// operation is skipped and `Ok` is returned. This is supposed to help callers to make copying
/// output optional. For example to skip copying back the output buffer of an `seal_call`
/// when the caller is not interested in the result.
@@ -553,7 +553,7 @@ where
allow_skip: bool,
create_token: impl FnOnce(u32) -> Option<RuntimeCosts>,
) -> Result<(), DispatchError> {
if allow_skip && out_ptr == u32::MAX {
if allow_skip && out_ptr == SENTINEL {
return Ok(())
}
@@ -648,48 +648,36 @@ 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() {
let max_size = self.ext.max_value_size();
let charged = self
.charge_gas(RuntimeCosts::SetStorage { new_bytes: value_len, old_bytes: max_size })?;
if value_len > max_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)
let write_outcome = self.ext.set_storage(key, value, false)?;
self.adjust_gas(
charged,
RuntimeCosts::SetStorage { new_bytes: value_len, old_bytes: write_outcome.old_len() },
);
Ok(write_outcome.old_len_with_sentinel())
}
fn clear_storage(&mut self, key_ptr: u32) -> Result<u32, TrapReason> {
self.charge_gas(RuntimeCosts::ClearStorage)?;
let charged = self.charge_gas(RuntimeCosts::ClearStorage(self.ext.max_value_size()))?;
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)
let outcome = self.ext.set_storage(key, None, false)?;
self.adjust_gas(charged, RuntimeCosts::ClearStorage(outcome.old_len()));
Ok(outcome.old_len_with_sentinel())
}
fn call(
@@ -827,7 +815,7 @@ define_env!(Env, <E: Ext>,
// # 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.
// `SENTINEL` 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)
},
@@ -849,7 +837,7 @@ define_env!(Env, <E: Ext>,
// # 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.
// `SENTINEL` is returned as a sentinel value.
[__unstable__] seal_clear_storage(ctx, key_ptr: u32) -> u32 => {
ctx.clear_storage(key_ptr).map_err(Into::into)
},
@@ -867,39 +855,39 @@ define_env!(Env, <E: Ext>,
//
// `ReturnCode::KeyNotFound`
[seal0] seal_get_storage(ctx, key_ptr: u32, out_ptr: u32, out_len_ptr: u32) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::GetStorageBase)?;
let charged = ctx.charge_gas(RuntimeCosts::GetStorage(ctx.ext.max_value_size()))?;
let mut key: StorageKey = [0; 32];
ctx.read_sandbox_memory_into_buf(key_ptr, &mut key)?;
if let Some(value) = ctx.ext.get_storage(&key) {
ctx.write_sandbox_output(out_ptr, out_len_ptr, &value, false, |len| {
Some(RuntimeCosts::GetStorageCopyOut(len))
})?;
ctx.adjust_gas(charged, RuntimeCosts::GetStorage(value.len() as u32));
ctx.write_sandbox_output(out_ptr, out_len_ptr, &value, false, already_charged)?;
Ok(ReturnCode::Success)
} else {
ctx.adjust_gas(charged, RuntimeCosts::GetStorage(0));
Ok(ReturnCode::KeyNotFound)
}
},
// 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
// # Return Value
//
// `ReturnCode::KeyNotFound`
[__unstable__] seal_contains_storage(ctx, key_ptr: u32) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::ContainsStorage)?;
// Returns the size of the pre-existing value at the specified key if any. Otherwise
// `SENTINEL` is returned as a sentinel value.
[__unstable__] seal_contains_storage(ctx, key_ptr: u32) -> u32 => {
let charged = ctx.charge_gas(RuntimeCosts::ContainsStorage(ctx.ext.max_value_size()))?;
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)
if let Some(len) = ctx.ext.get_storage_size(&key) {
ctx.adjust_gas(charged, RuntimeCosts::ContainsStorage(len));
Ok(len)
} else {
Ok(ReturnCode::KeyNotFound)
ctx.adjust_gas(charged, RuntimeCosts::ContainsStorage(0));
Ok(SENTINEL)
}
},
@@ -916,15 +904,15 @@ define_env!(Env, <E: Ext>,
//
// `ReturnCode::KeyNotFound`
[__unstable__] seal_take_storage(ctx, key_ptr: u32, out_ptr: u32, out_len_ptr: u32) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::TakeStorageBase)?;
let charged = ctx.charge_gas(RuntimeCosts::TakeStorage(ctx.ext.max_value_size()))?;
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))
})?;
if let crate::storage::WriteOutcome::Taken(value) = ctx.ext.set_storage(key, None, true)? {
ctx.adjust_gas(charged, RuntimeCosts::TakeStorage(value.len() as u32));
ctx.write_sandbox_output(out_ptr, out_len_ptr, &value, false, already_charged)?;
Ok(ReturnCode::Success)
} else {
ctx.adjust_gas(charged, RuntimeCosts::TakeStorage(0));
Ok(ReturnCode::KeyNotFound)
}
},
@@ -1006,7 +994,7 @@ define_env!(Env, <E: Ext>,
//
// The callees output buffer is copied to `output_ptr` and its length to `output_len_ptr`.
// The copy of the output buffer can be skipped by supplying the sentinel value
// of `u32::MAX` to `output_ptr`.
// of `SENTINEL` to `output_ptr`.
//
// # Parameters
//
@@ -1103,7 +1091,7 @@ define_env!(Env, <E: Ext>,
// by the code hash. The address of this new account is copied to `address_ptr` and its length
// to `address_len_ptr`. The constructors output buffer is copied to `output_ptr` and its
// length to `output_len_ptr`. The copy of the output buffer and address can be skipped by
// supplying the sentinel value of `u32::MAX` to `output_ptr` or `address_ptr`.
// supplying the sentinel value of `SENTINEL` to `output_ptr` or `address_ptr`.
//
// `value` must be at least the minimum balance. Otherwise the instantiation fails and the
// contract is not created.