Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
24 changes: 13 additions & 11 deletions frame/contracts/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1336,7 +1336,18 @@ where

fn append_debug_buffer(&mut self, msg: &str) -> bool {
if let Some(buffer) = &mut self.debug_message {
let mut msg = msg.bytes();
let err_msg = scale_info::prelude::format!(
"Debug message too big (size={}) for debug buffer (bound={})",
msg.len(),
DebugBufferVec::<T>::bound(),
);

let mut msg = if msg.len() > DebugBufferVec::<T>::bound() {
err_msg.bytes()
} else {
msg.bytes()
};

let num_drain = {
let capacity = DebugBufferVec::<T>::bound().checked_sub(buffer.len()).expect(
"
Expand All @@ -1349,16 +1360,7 @@ where
msg.len().saturating_sub(capacity).min(buffer.len())
};
buffer.drain(0..num_drain);
buffer
.try_extend(&mut msg)
.map_err(|_| {
log::debug!(
target: "runtime::contracts",
"Debug message to big (size={}) for debug buffer (bound={})",
msg.len(), DebugBufferVec::<T>::bound(),
);
})
.ok();
buffer.try_extend(&mut msg).ok();
true
} else {
false
Expand Down
61 changes: 61 additions & 0 deletions frame/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ use pallet_contracts_primitives::{
StorageDeposit,
};
use scale_info::TypeInfo;
use smallvec::Array;
use sp_runtime::traits::{Convert, Hash, Saturating, StaticLookup, TrailingZeroInput};
use sp_std::{fmt::Debug, marker::PhantomData, prelude::*};

Expand Down Expand Up @@ -272,6 +273,9 @@ pub mod pallet {
/// The allowed depth is `CallStack::size() + 1`.
/// Therefore a size of `0` means that a contract cannot use call or instantiate.
/// In other words only the origin called "root contract" is allowed to execute then.
///
/// This setting along with [`MaxCodeLen`](#associatedtype.MaxCodeLen) directly affects
/// memory usage of your runtime.
type CallStack: smallvec::Array<Item = Frame<Self>>;

/// The maximum number of contracts that can be pending for deletion.
Expand Down Expand Up @@ -323,6 +327,10 @@ pub mod pallet {
/// The maximum length of a contract code in bytes. This limit applies to the instrumented
/// version of the code. Therefore `instantiate_with_code` can fail even when supplying
/// a wasm binary below this maximum size.
///
/// The value should be chosen carefully taking into the account the overall memory limit
/// your runtime has, as well as the [maximum allowed callstack
/// depth](#associatedtype.CallStack). Look into the `integrity_test()` for some insights.
#[pallet::constant]
type MaxCodeLen: Get<u32>;

Expand Down Expand Up @@ -372,6 +380,59 @@ pub mod pallet {
T::WeightInfo::on_process_deletion_queue_batch()
}
}

fn integrity_test() {
// Total runtime memory is expected to have 1Gb upper limit
const MAX_RUNTIME_MEM: u32 = 1024 * 1024 * 1024;
// Memory limits for a single contract:
// Value stack size: 1Mb per contract, default defined in wasmi
const STACK_MAX_SIZE: u32 = 1024 * 1024;
// Heap limit is normally 16 mempages of 64kb each = 1Mb per contract
let heap_max_size = T::Schedule::get().limits.max_memory_size();
let stack_height = T::CallStack::size() as u32;
// Check that given configured `MaxCodeLen`, runtime heap memory limit can't be broken.
//
// In worst case, the decoded wasm contract code would be `x16` times larger than the
// encoded one. This is because even a single-byte wasm instruction has 16-byte size in
// wasmi. This gives us `MaxCodeLen*16` safety margin.
//
// Next, the pallet keeps both the original and instrumented wasm blobs for each
// contract, hence we add up `MaxCodeLen*2` more to the safety margin.
//
// Finally, the inefficiencies of the freeing-bump allocator
// being used in the client for the runtime memory allocations, could lead to possible
// memory allocations for contract code grow up to `x4` times in some extreme cases,
// wich gives us total multiplier of `18*4` for `MaxCodeLen`.
//
// That being said, for every contract executed in runtime, at least `MaxCodeLen*18*4`
// memory should be avaiable. Note that maximum allowed heap memory and stack size per
// each contract (stack frame) should also be counted.
//
// Finally, we allow 50% of the runtime memory to be utilized by the contracts call
// stack, keeping the rest for other facilities, such as PoV, etc.
//
// This gives us the following formula:
//
// `(MaxCodeLen * 18 * 4 + STACK_MAX_SIZE + heap_max_size) * stack_height <
// MAX_RUNTIME_MEM/2`
//
// Hence the upper limit for the `MaxCodeLen` can be defined as follows:
let code_len_limit = MAX_RUNTIME_MEM
.saturating_div(2)
.saturating_div(stack_height)
.saturating_sub(heap_max_size)
.saturating_sub(STACK_MAX_SIZE)
.saturating_div(18 * 4);

assert!(
T::MaxCodeLen::get() < code_len_limit,
"Given `CallStack` height {:?}, `MaxCodeLen` should be set less than {:?} \
(current value is {:?}), to avoid possible runtime oom issues.",
stack_height,
code_len_limit,
T::MaxCodeLen::get(),
);
}
}

#[pallet::call]
Expand Down