contracts: Add automated weights for wasm instructions (#7361)

* pallet_contracts: Inline benchmark helper that is only used once

* Move all max_* Schedule items into a new struct

* Limit the number of globals a module can declare

* The current limits are too high for wasmi to even execute

* Limit the amount of parameters any wasm function is allowed to have

* Limit the size the BrTable's immediate value

* Add instruction benchmarks

* Add new benchmarks to the schedule and make use of it

* Add Benchmark Results generated by the bench bot

* Add proc macro that implements `Debug` for `Schedule`

* Add missing imports necessary for no_std build

* Make the WeightDebug macro available for no_std

In this case a dummy implementation is derived in order to not
blow up the code size akin to the RuntimeDebug macro.

* Rework instr_memory_grow benchmark to use only the maximum amount of pages allowed

* Add maximum amount of memory when benching (seal_)call/instantiate

* 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 --heap-pages=4096 --output ./bin/node/runtime/src/weights --header ./HEADER --pallet pallet_contracts

* Added utility benchmark that allows pretty printing of the real schedule

* review: Add missing header to the proc-macro lib.rs

* review: Clarify why #[allow(dead_code)] attribute is there

* review: Fix pwasm-utils line

* review: Fixup rand usage

* review: Fix typo

* review: Imported -> Exported

* cargo run --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

* contracts: Adapt to new weight structure

* contracts: Fixup runtime WeightInfo

* contracts: Remove unneeded fullpath of WeightInfo type

* Apply suggestions from code review

Co-authored-by: Andrew Jones <ascjones@gmail.com>

* Fix typo in schedule.rs

Co-authored-by: Andrew Jones <ascjones@gmail.com>

* Fix docs in schedule.rs

* Apply suggestions from code review

Co-authored-by: Nikolay Volf <nikvolf@gmail.com>

* Don't publish proc-macro crate until 3.0.0 is ready

* Optimize imports for less repetition

* Break overlong line

Co-authored-by: Parity Benchmarking Bot <admin@parity.io>
Co-authored-by: Andrew Jones <ascjones@gmail.com>
Co-authored-by: Nikolay Volf <nikvolf@gmail.com>
This commit is contained in:
Alexander Theißen
2020-11-09 15:32:14 +01:00
committed by GitHub
parent 9704c204e6
commit 51c67fe881
17 changed files with 3152 additions and 843 deletions
+9 -8
View File
@@ -164,6 +164,7 @@ mod tests {
use hex_literal::hex;
use sp_runtime::DispatchError;
use frame_support::weights::Weight;
use assert_matches::assert_matches;
use pallet_contracts_primitives::{ExecReturnValue, ReturnFlags, ExecError, ErrorOrigin};
const GAS_LIMIT: Gas = 10_000_000_000;
@@ -645,14 +646,14 @@ mod tests {
&mut GasMeter::new(GAS_LIMIT),
).unwrap();
assert_eq!(
&mock_ext.instantiates,
&[InstantiateEntry {
code_hash: [0x11; 32].into(),
assert_matches!(
&mock_ext.instantiates[..],
[InstantiateEntry {
code_hash,
endowment: 3,
data: vec![1, 2, 3, 4],
gas_left: 9392302058,
}]
data,
gas_left: _,
}] if code_hash == &[0x11; 32].into() && data == &vec![1, 2, 3, 4]
);
}
@@ -1461,7 +1462,7 @@ mod tests {
vec![0x00, 0x01, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x14, 0x00])
]);
assert_eq!(gas_meter.gas_left(), 9834099446);
assert!(gas_meter.gas_left() > 0);
}
const CODE_DEPOSIT_EVENT_MAX_TOPICS: &str = r#"
+163 -29
View File
@@ -24,9 +24,7 @@ use crate::{Schedule, Trait};
use parity_wasm::elements::{self, Internal, External, MemoryType, Type, ValueType};
use pwasm_utils;
use pwasm_utils::rules;
use sp_std::prelude::*;
use sp_runtime::traits::{SaturatedConversion};
/// Currently, all imported functions must be located inside this module. We might support
/// additional modules for versioning later.
@@ -101,6 +99,33 @@ impl<'a, T: Trait> ContractModule<'a, T> {
Ok(())
}
/// Ensure that any `br_table` instruction adheres to its immediate value limit.
fn ensure_br_table_size_limit(&self, limit: u32) -> Result<(), &'static str> {
let code_section = if let Some(type_section) = self.module.code_section() {
type_section
} else {
return Ok(());
};
for instr in code_section.bodies().iter().flat_map(|body| body.code().elements()) {
use parity_wasm::elements::Instruction::BrTable;
if let BrTable(table) = instr {
if table.table.len() > limit as usize {
return Err("BrTable's immediate value is too big.")
}
}
}
Ok(())
}
fn ensure_global_variable_limit(&self, limit: u32) -> Result<(), &'static str> {
if let Some(global_section) = self.module.global_section() {
if global_section.entries().len() > limit as usize {
return Err("module declares too many globals")
}
}
Ok(())
}
/// Ensures that no floating point types are in use.
fn ensure_no_floating_types(&self) -> Result<(), &'static str> {
if let Some(global_section) = self.module.global_section() {
@@ -145,15 +170,25 @@ impl<'a, T: Trait> ContractModule<'a, T> {
Ok(())
}
fn inject_gas_metering(self) -> Result<Self, &'static str> {
let gas_rules =
rules::Set::new(
self.schedule.instruction_weights.regular.clone().saturated_into(),
Default::default(),
)
.with_grow_cost(self.schedule.instruction_weights.grow_mem.clone().saturated_into())
.with_forbidden_floats();
/// Ensure that no function exists that has more parameters than allowed.
fn ensure_parameter_limit(&self, limit: u32) -> Result<(), &'static str> {
let type_section = if let Some(type_section) = self.module.type_section() {
type_section
} else {
return Ok(());
};
for Type::Function(func) in type_section.types() {
if func.params().len() > limit as usize {
return Err("Use of a function type with too many parameters.");
}
}
Ok(())
}
fn inject_gas_metering(self) -> Result<Self, &'static str> {
let gas_rules = self.schedule.rules(&self.module);
let contract_module = pwasm_utils::inject_gas_counter(
self.module,
&gas_rules,
@@ -167,7 +202,8 @@ impl<'a, T: Trait> ContractModule<'a, T> {
fn inject_stack_height_metering(self) -> Result<Self, &'static str> {
let contract_module =
pwasm_utils::stack_height::inject_limiter(self.module, self.schedule.max_stack_height)
pwasm_utils::stack_height
::inject_limiter(self.module, self.schedule.limits.stack_height)
.map_err(|_| "stack height instrumentation failed")?;
Ok(ContractModule {
module: contract_module,
@@ -345,7 +381,7 @@ fn get_memory_limits<T: Trait>(module: Option<&MemoryType>, schedule: &Schedule<
"Requested initial number of pages should not exceed the requested maximum",
);
}
(_, Some(maximum)) if maximum > schedule.max_memory_pages => {
(_, Some(maximum)) if maximum > schedule.limits.memory_pages => {
return Err("Maximum number of pages should not exceed the configured maximum.");
}
(initial, Some(maximum)) => Ok((initial, maximum)),
@@ -381,8 +417,11 @@ pub fn prepare_contract<C: ImportSatisfyCheck, T: Trait>(
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_table_size_limit(schedule.limits.table_size)?;
contract_module.ensure_global_variable_limit(schedule.limits.globals)?;
contract_module.ensure_no_floating_types()?;
contract_module.ensure_parameter_limit(schedule.limits.parameters)?;
contract_module.ensure_br_table_size_limit(schedule.limits.br_table_size)?;
// We disallow importing `gas` function here since it is treated as implementation detail.
let disallowed_imports = [b"gas".as_ref()];
@@ -442,7 +481,7 @@ pub mod benchmarking {
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::Ext;
use crate::{exec::Ext, Limits};
use std::fmt;
use assert_matches::assert_matches;
@@ -470,7 +509,17 @@ mod tests {
#[test]
fn $name() {
let wasm = wat::parse_str($wat).unwrap();
let schedule = Schedule::default();
let schedule = Schedule {
limits: Limits {
globals: 3,
parameters: 3,
memory_pages: 16,
table_size: 3,
br_table_size: 3,
.. Default::default()
},
.. Default::default()
};
let r = prepare_contract::<TestEnv, crate::tests::Test>(wasm.as_ref(), &schedule);
assert_matches!(r, $($expected)*);
}
@@ -493,14 +542,66 @@ mod tests {
Err("gas instrumentation failed")
);
mod memories {
mod functions {
use super::*;
// Tests below assumes that maximum page number is configured to a certain number.
#[test]
fn assume_memory_size() {
assert_eq!(<Schedule<crate::tests::Test>>::default().max_memory_pages, 16);
}
prepare_test!(param_number_valid,
r#"
(module
(func (export "call"))
(func (export "deploy"))
(func (param i32 i32 i32))
)
"#,
Ok(_)
);
prepare_test!(param_number_invalid,
r#"
(module
(func (export "call"))
(func (export "deploy"))
(func (param i32 i32 i32 i32))
(func (param i32))
)
"#,
Err("Use of a function type with too many parameters.")
);
}
mod globals {
use super::*;
prepare_test!(global_number_valid,
r#"
(module
(global i64 (i64.const 0))
(global i64 (i64.const 0))
(global i64 (i64.const 0))
(func (export "call"))
(func (export "deploy"))
)
"#,
Ok(_)
);
prepare_test!(global_number_too_high,
r#"
(module
(global i64 (i64.const 0))
(global i64 (i64.const 0))
(global i64 (i64.const 0))
(global i64 (i64.const 0))
(func (export "call"))
(func (export "deploy"))
)
"#,
Err("module declares too many globals")
);
}
mod memories {
use super::*;
prepare_test!(memory_with_one_page,
r#"
@@ -561,6 +662,18 @@ mod tests {
Err("Maximum number of pages should be always declared.")
);
prepare_test!(requested_maximum_valid,
r#"
(module
(import "env" "memory" (memory 1 16))
(func (export "call"))
(func (export "deploy"))
)
"#,
Ok(_)
);
prepare_test!(requested_maximum_exceeds_configured_maximum,
r#"
(module
@@ -625,12 +738,6 @@ mod tests {
mod tables {
use super::*;
// Tests below assumes that maximum table size is configured to a certain number.
#[test]
fn assume_table_size() {
assert_eq!(<Schedule<crate::tests::Test>>::default().max_table_size, 16384);
}
prepare_test!(no_tables,
r#"
(module
@@ -644,7 +751,7 @@ mod tests {
prepare_test!(table_valid_size,
r#"
(module
(table 10000 funcref)
(table 3 funcref)
(func (export "call"))
(func (export "deploy"))
@@ -656,13 +763,40 @@ mod tests {
prepare_test!(table_too_big,
r#"
(module
(table 20000 funcref)
(table 4 funcref)
(func (export "call"))
(func (export "deploy"))
)"#,
Err("table exceeds maximum size allowed")
);
prepare_test!(br_table_valid_size,
r#"
(module
(func (export "call"))
(func (export "deploy"))
(func
i32.const 0
br_table 0 0 0 0
)
)
"#,
Ok(_)
);
prepare_test!(br_table_too_big,
r#"
(module
(func (export "call"))
(func (export "deploy"))
(func
i32.const 0
br_table 0 0 0 0 0
)
)"#,
Err("BrTable's immediate value is too big.")
);
}
mod imports {
@@ -279,7 +279,7 @@ pub enum RuntimeToken {
}
impl<T: Trait> Token<T> for RuntimeToken {
type Metadata = HostFnWeights;
type Metadata = HostFnWeights<T>;
fn calculate_amount(&self, s: &Self::Metadata) -> Gas {
use self::RuntimeToken::*;
@@ -340,7 +340,7 @@ impl<T: Trait> Token<T> for RuntimeToken {
fn charge_gas<E, Tok>(ctx: &mut Runtime<E>, token: Tok) -> Result<(), sp_sandbox::HostError>
where
E: Ext,
Tok: Token<E::T, Metadata=HostFnWeights>,
Tok: Token<E::T, Metadata=HostFnWeights<E::T>>,
{
match ctx.gas_meter.charge(&ctx.schedule.host_fn_weights, token) {
GasMeterResult::Proceed => Ok(()),
@@ -1024,8 +1024,7 @@ define_env!(Env, <E: Ext>,
// The data is encoded as T::Hash.
seal_random(ctx, subject_ptr: u32, subject_len: u32, out_ptr: u32, out_len_ptr: u32) => {
charge_gas(ctx, RuntimeToken::Random)?;
// The length of a subject can't exceed `max_subject_len`.
if subject_len > ctx.schedule.max_subject_len {
if subject_len > ctx.schedule.limits.subject_len {
return Err(sp_sandbox::HostError);
}
let subject_buf = read_sandbox_memory(ctx, subject_ptr, subject_len)?;
@@ -1157,7 +1156,7 @@ define_env!(Env, <E: Ext>,
},
// Deposit a contract event with the data buffer and optional list of topics. There is a limit
// on the maximum number of topics specified by `max_event_topics`.
// on the maximum number of topics specified by `event_topics`.
//
// - topics_ptr - a pointer to the buffer of topics encoded as `Vec<T::Hash>`. The value of this
// is ignored if `topics_len` is set to 0. The topics list can't contain duplicates.
@@ -1181,8 +1180,8 @@ define_env!(Env, <E: Ext>,
_ => read_sandbox_memory_as(ctx, topics_ptr, topics_len)?,
};
// If there are more than `max_event_topics`, then trap.
if topics.len() > ctx.schedule.max_event_topics as usize {
// If there are more than `event_topics`, then trap.
if topics.len() > ctx.schedule.limits.event_topics as usize {
return Err(sp_sandbox::HostError);
}