seal: Add automated weights for contract API calls (#7017)

* seal: Add capability to put uninstrumented code (for benchmarks)

Benchmarks should only measure the overhead of the API calls itself.
For that reason we want to run them without instrumentation.

* seal: Cap the the data length for deposited events

Data used in events has storage implications for archive nodes.
Those need to keep the events in storage forever. For that reason
we want to limit the amount of storage that can be used inside events.

* seal: Fix error reporting in the case out of bound sandbox access

* seal: Refactor existing benchmarks

* seal: Convert benchmark file to tabs

* seal: Add benchmarks for functions called by contracts

* seal: Create a default schedule from benchmark generated WeightInfo

* seal: Make use of WeightInfo in extrinsic weight annotations

* seal: Replace the old schedule by the benchmark generated one

* Review: Fix copy paste typo in schedule construction

* Review: Fix stale docs

* Fix whitespace errors

Co-authored-by: Sergei Shulepov <sergei@parity.io>

* Review: Use checked_div in order to be more defensive

* Review: Rename no_charge to already_charged

* Review: Whitelist caller of extrinsics

* Review: Remove trailing whitespace

* Review: Remove confusing "self::" syntax

* Review: Add docs for the benchmark prepration submodule

* Review: Move code generation functions to own module

* Review: Refactor and document benchmark helper functions

* Remove additional empty line

* Added missing comment on caller_funding

* Update frame/contracts/src/benchmarking/code.rs

Co-authored-by: Sergei Shulepov <sergei@parity.io>

* Fix missing sp_std::prelude import in code.rs

* cargo run --release --features runtime-benchmarks --manifest-path bin/node/cli/Cargo.toml -- benchmark --chain dev --steps 50 --repeat 20 --extrinsic * --execution=wasm --wasm-execution=compiled --output ./bin/node/runtime/src/weights --header ./HEADER --pallet pallet_contracts --heap-pages 4096

* Use weights from the benchmark machine for the substrate node

* Remove prefixes from Schedule members

* Data lengths in the WeightInfo Trait are specified in kilobytes

* Rename ApiWeights to HostFunctionWeights

Co-authored-by: Sergei Shulepov <sergei@parity.io>
Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
Alexander Theißen
2020-10-08 17:36:37 +02:00
committed by GitHub
parent e3682fa2f4
commit 443725f0f6
20 changed files with 3460 additions and 920 deletions
+89 -49
View File
@@ -20,7 +20,7 @@
use crate::wasm::env_def::ImportSatisfyCheck;
use crate::wasm::PrefabWasmModule;
use crate::Schedule;
use crate::{Schedule, Trait};
use parity_wasm::elements::{self, Internal, External, MemoryType, Type, ValueType};
use pwasm_utils;
@@ -36,20 +36,20 @@ pub const IMPORT_MODULE_FN: &str = "seal0";
/// compiler toolchains might not support specifying other modules than "env" for memory imports.
pub const IMPORT_MODULE_MEMORY: &str = "env";
struct ContractModule<'a> {
struct ContractModule<'a, T: Trait> {
/// A deserialized module. The module is valid (this is Guaranteed by `new` method).
module: elements::Module,
schedule: &'a Schedule,
schedule: &'a Schedule<T>,
}
impl<'a> ContractModule<'a> {
impl<'a, T: Trait> ContractModule<'a, T> {
/// Creates a new instance of `ContractModule`.
///
/// Returns `Err` if the `original_code` couldn't be decoded or
/// if it contains an invalid module.
fn new(
original_code: &[u8],
schedule: &'a Schedule,
schedule: &'a Schedule<T>,
) -> Result<Self, &'static str> {
use wasmi_validation::{validate_module, PlainValidator};
@@ -148,10 +148,10 @@ impl<'a> ContractModule<'a> {
fn inject_gas_metering(self) -> Result<Self, &'static str> {
let gas_rules =
rules::Set::new(
self.schedule.regular_op_cost.clone().saturated_into(),
self.schedule.instruction_weights.regular.clone().saturated_into(),
Default::default(),
)
.with_grow_cost(self.schedule.grow_mem_cost.clone().saturated_into())
.with_grow_cost(self.schedule.instruction_weights.grow_mem.clone().saturated_into())
.with_forbidden_floats();
let contract_module = pwasm_utils::inject_gas_counter(
@@ -269,7 +269,10 @@ impl<'a> ContractModule<'a> {
/// - checks any imported function against defined host functions set, incl.
/// their signatures.
/// - if there is a memory import, returns it's descriptor
fn scan_imports<C: ImportSatisfyCheck>(&self) -> Result<Option<&MemoryType>, &'static str> {
/// `import_fn_banlist`: list of function names that are disallowed to be imported
fn scan_imports<C: ImportSatisfyCheck>(&self, import_fn_banlist: &[&[u8]])
-> Result<Option<&MemoryType>, &'static str>
{
let module = &self.module;
let types = module.type_section().map(|ts| ts.types()).unwrap_or(&[]);
@@ -315,8 +318,7 @@ impl<'a> ContractModule<'a> {
return Err("module imports `seal_println` but debug features disabled");
}
// We disallow importing `gas` function here since it is treated as implementation detail.
if import.field().as_bytes() == b"gas"
if import_fn_banlist.iter().any(|f| import.field().as_bytes() == *f)
|| !C::can_satisfy(import.field().as_bytes(), func_ty)
{
return Err("module imports a non-existent function");
@@ -331,33 +333,10 @@ impl<'a> ContractModule<'a> {
}
}
/// Loads the given module given in `original_code`, performs some checks on it and
/// does some preprocessing.
///
/// The checks are:
///
/// - provided code is a valid wasm module.
/// - the module doesn't define an internal memory instance,
/// - imported memory (if any) doesn't reserve more memory than permitted by the `schedule`,
/// - all imported functions from the external environment matches defined by `env` module,
///
/// The preprocessing includes injecting code for gas metering and metering the height of stack.
pub fn prepare_contract<C: ImportSatisfyCheck>(
original_code: &[u8],
schedule: &Schedule,
) -> Result<PrefabWasmModule, &'static str> {
let mut contract_module = ContractModule::new(original_code, schedule)?;
contract_module.scan_exports()?;
contract_module.ensure_no_internal_memory()?;
contract_module.ensure_table_size_limit(schedule.max_table_size)?;
contract_module.ensure_no_floating_types()?;
struct MemoryDefinition {
initial: u32,
maximum: u32,
}
let memory_def = if let Some(memory_type) = contract_module.scan_imports::<C>()? {
fn get_memory_limits<T: Trait>(module: Option<&MemoryType>, schedule: &Schedule<T>)
-> Result<(u32, u32), &'static str>
{
if let Some(memory_type) = module {
// Inspect the module to extract the initial and maximum page count.
let limits = memory_type.limits();
match (limits.initial(), limits.maximum()) {
@@ -369,7 +348,7 @@ pub fn prepare_contract<C: ImportSatisfyCheck>(
(_, Some(maximum)) if maximum > schedule.max_memory_pages => {
return Err("Maximum number of pages should not exceed the configured maximum.");
}
(initial, Some(maximum)) => MemoryDefinition { initial, maximum },
(initial, Some(maximum)) => Ok((initial, maximum)),
(_, None) => {
// Maximum number of pages should be always declared.
// This isn't a hard requirement and can be treated as a maximum set
@@ -380,11 +359,37 @@ pub fn prepare_contract<C: ImportSatisfyCheck>(
} else {
// If none memory imported then just crate an empty placeholder.
// Any access to it will lead to out of bounds trap.
MemoryDefinition {
initial: 0,
maximum: 0,
}
};
Ok((0, 0))
}
}
/// Loads the given module given in `original_code`, performs some checks on it and
/// does some preprocessing.
///
/// The checks are:
///
/// - provided code is a valid wasm module.
/// - the module doesn't define an internal memory instance,
/// - imported memory (if any) doesn't reserve more memory than permitted by the `schedule`,
/// - all imported functions from the external environment matches defined by `env` module,
///
/// The preprocessing includes injecting code for gas metering and metering the height of stack.
pub fn prepare_contract<C: ImportSatisfyCheck, T: Trait>(
original_code: &[u8],
schedule: &Schedule<T>,
) -> Result<PrefabWasmModule, &'static str> {
let mut contract_module = ContractModule::new(original_code, schedule)?;
contract_module.scan_exports()?;
contract_module.ensure_no_internal_memory()?;
contract_module.ensure_table_size_limit(schedule.max_table_size)?;
contract_module.ensure_no_floating_types()?;
// We disallow importing `gas` function here since it is treated as implementation detail.
let disallowed_imports = [b"gas".as_ref()];
let memory_limits = get_memory_limits(
contract_module.scan_imports::<C>(&disallowed_imports)?,
schedule
)?;
contract_module = contract_module
.inject_gas_metering()?
@@ -392,13 +397,48 @@ pub fn prepare_contract<C: ImportSatisfyCheck>(
Ok(PrefabWasmModule {
schedule_version: schedule.version,
initial: memory_def.initial,
maximum: memory_def.maximum,
initial: memory_limits.0,
maximum: memory_limits.1,
_reserved: None,
code: contract_module.into_wasm_code()?,
})
}
/// Alternate (possibly unsafe) preparation functions used only for benchmarking.
///
/// For benchmarking we need to construct special contracts that might not pass our
/// sanity checks or need to skip instrumentation for correct results. We hide functions
/// allowing this behind a feature that is only set during benchmarking to prevent usage
/// in production code.
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking {
use super::{
Trait, ContractModule, PrefabWasmModule, ImportSatisfyCheck, Schedule, get_memory_limits
};
use parity_wasm::elements::FunctionType;
impl ImportSatisfyCheck for () {
fn can_satisfy(_name: &[u8], _func_type: &FunctionType) -> bool {
true
}
}
/// Prepare function that neither checks nor instruments the passed in code.
pub fn prepare_contract<T: Trait>(original_code: &[u8], schedule: &Schedule<T>)
-> Result<PrefabWasmModule, &'static str>
{
let contract_module = ContractModule::new(original_code, schedule)?;
let memory_limits = get_memory_limits(contract_module.scan_imports::<()>(&[])?, schedule)?;
Ok(PrefabWasmModule {
schedule_version: schedule.version,
initial: memory_limits.0,
maximum: memory_limits.1,
_reserved: None,
code: contract_module.into_wasm_code()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -431,7 +471,7 @@ mod tests {
fn $name() {
let wasm = wat::parse_str($wat).unwrap();
let schedule = Schedule::default();
let r = prepare_contract::<TestEnv>(wasm.as_ref(), &schedule);
let r = prepare_contract::<TestEnv, crate::tests::Test>(wasm.as_ref(), &schedule);
assert_matches!(r, $($expected)*);
}
};
@@ -459,7 +499,7 @@ mod tests {
// Tests below assumes that maximum page number is configured to a certain number.
#[test]
fn assume_memory_size() {
assert_eq!(Schedule::default().max_memory_pages, 16);
assert_eq!(<Schedule<crate::tests::Test>>::default().max_memory_pages, 16);
}
prepare_test!(memory_with_one_page,
@@ -588,7 +628,7 @@ mod tests {
// Tests below assumes that maximum table size is configured to a certain number.
#[test]
fn assume_table_size() {
assert_eq!(Schedule::default().max_table_size, 16384);
assert_eq!(<Schedule<crate::tests::Test>>::default().max_table_size, 16384);
}
prepare_test!(no_tables,
@@ -757,7 +797,7 @@ mod tests {
).unwrap();
let mut schedule = Schedule::default();
schedule.enable_println = true;
let r = prepare_contract::<TestEnv>(wasm.as_ref(), &schedule);
let r = prepare_contract::<TestEnv, crate::tests::Test>(wasm.as_ref(), &schedule);
assert_matches!(r, Ok(_));
}
}