referrerpolicy=no-referrer-when-downgrade

pallet_contracts/benchmarking/
code.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Functions to procedurally construct contract code used for benchmarking.
19//!
20//! In order to be able to benchmark events that are triggered by contract execution
21//! (API calls into seal, individual instructions), we need to generate contracts that
22//! perform those events. Because those contracts can get very big we cannot simply define
23//! them as text (.wat) as this will be too slow and consume too much memory. Therefore
24//! we define this simple definition of a contract that can be passed to `create_code` that
25//! compiles it down into a `WasmModule` that can be used as a contract's code.
26
27use crate::Config;
28use alloc::{borrow::ToOwned, vec, vec::Vec};
29use frame_support::traits::Get;
30use sp_runtime::{traits::Hash, Saturating};
31use wasm_instrument::parity_wasm::{
32	builder,
33	elements::{
34		self, BlockType, CustomSection, FuncBody, Instruction, Instructions, Local, Section,
35		ValueType,
36	},
37};
38
39/// The location where to put the generated code.
40pub enum Location {
41	/// Generate all code into the `call` exported function.
42	Call,
43	/// Generate all code into the `deploy` exported function.
44	Deploy,
45}
46
47/// Pass to `create_code` in order to create a compiled `WasmModule`.
48///
49/// This exists to have a more declarative way to describe a wasm module than to use
50/// parity-wasm directly. It is tailored to fit the structure of contracts that are
51/// needed for benchmarking.
52#[derive(Default)]
53pub struct ModuleDefinition {
54	/// Imported memory attached to the module. No memory is imported if `None`.
55	pub memory: Option<ImportedMemory>,
56	/// Initializers for the imported memory.
57	pub data_segments: Vec<DataSegment>,
58	/// Creates the supplied amount of i64 mutable globals initialized with random values.
59	pub num_globals: u32,
60	/// List of functions that the module should import. They start with index 0.
61	pub imported_functions: Vec<ImportedFunction>,
62	/// Function body of the exported `deploy` function. Body is empty if `None`.
63	/// Its index is `imported_functions.len()`.
64	pub deploy_body: Option<FuncBody>,
65	/// Function body of the exported `call` function. Body is empty if `None`.
66	/// Its index is `imported_functions.len() + 1`.
67	pub call_body: Option<FuncBody>,
68	/// Function body of a non-exported function with index `imported_functions.len() + 2`.
69	pub aux_body: Option<FuncBody>,
70	/// The amount of I64 arguments the aux function should have.
71	pub aux_arg_num: u32,
72	/// Create a table containing function pointers.
73	pub table: Option<TableSegment>,
74	/// Create a section named "dummy" of the specified size. This is useful in order to
75	/// benchmark the overhead of loading and storing codes of specified sizes. The dummy
76	/// section only contributes to the size of the contract but does not affect execution.
77	pub dummy_section: u32,
78}
79
80pub struct TableSegment {
81	/// How many elements should be created inside the table.
82	pub num_elements: u32,
83	/// The function index with which all table elements should be initialized.
84	pub function_index: u32,
85}
86
87pub struct DataSegment {
88	pub offset: u32,
89	pub value: Vec<u8>,
90}
91
92#[derive(Clone)]
93pub struct ImportedMemory {
94	pub min_pages: u32,
95	pub max_pages: u32,
96}
97
98impl ImportedMemory {
99	pub fn max<T: Config>() -> Self {
100		let pages = max_pages::<T>();
101		Self { min_pages: pages, max_pages: pages }
102	}
103}
104
105pub struct ImportedFunction {
106	pub module: &'static str,
107	pub name: &'static str,
108	pub params: Vec<ValueType>,
109	pub return_type: Option<ValueType>,
110}
111
112/// A wasm module ready to be put on chain.
113#[derive(Clone)]
114pub struct WasmModule<T: Config> {
115	pub code: Vec<u8>,
116	pub hash: <T::Hashing as Hash>::Output,
117}
118
119impl<T: Config> From<ModuleDefinition> for WasmModule<T> {
120	fn from(def: ModuleDefinition) -> Self {
121		// internal functions start at that offset.
122		let func_offset = u32::try_from(def.imported_functions.len()).unwrap();
123
124		// Every contract must export "deploy" and "call" functions.
125		let mut contract = builder::module()
126			// deploy function (first internal function)
127			.function()
128			.signature()
129			.build()
130			.with_body(
131				def.deploy_body
132					.unwrap_or_else(|| FuncBody::new(Vec::new(), Instructions::empty())),
133			)
134			.build()
135			// call function (second internal function)
136			.function()
137			.signature()
138			.build()
139			.with_body(
140				def.call_body
141					.unwrap_or_else(|| FuncBody::new(Vec::new(), Instructions::empty())),
142			)
143			.build()
144			.export()
145			.field("deploy")
146			.internal()
147			.func(func_offset)
148			.build()
149			.export()
150			.field("call")
151			.internal()
152			.func(func_offset + 1)
153			.build();
154
155		// If specified we add an additional internal function
156		if let Some(body) = def.aux_body {
157			let mut signature = contract.function().signature();
158			for _ in 0..def.aux_arg_num {
159				signature = signature.with_param(ValueType::I64);
160			}
161			contract = signature.build().with_body(body).build();
162		}
163
164		// Grant access to linear memory.
165		// Every contract module is required to have an imported memory.
166		// If no memory is specified in the passed ModuleDefinition, then
167		// default to (1, 1).
168		let (init, max) = if let Some(memory) = &def.memory {
169			(memory.min_pages, Some(memory.max_pages))
170		} else {
171			(1, Some(1))
172		};
173
174		contract = contract.import().path("env", "memory").external().memory(init, max).build();
175
176		// Import supervisor functions. They start with idx 0.
177		for func in def.imported_functions {
178			let sig = builder::signature()
179				.with_params(func.params)
180				.with_results(func.return_type)
181				.build_sig();
182			let sig = contract.push_signature(sig);
183			contract = contract
184				.import()
185				.module(func.module)
186				.field(func.name)
187				.with_external(elements::External::Function(sig))
188				.build();
189		}
190
191		// Initialize memory
192		for data in def.data_segments {
193			contract = contract
194				.data()
195				.offset(Instruction::I32Const(data.offset as i32))
196				.value(data.value)
197				.build()
198		}
199
200		// Add global variables
201		if def.num_globals > 0 {
202			use rand::{distributions::Standard, prelude::*};
203			let rng = rand_pcg::Pcg32::seed_from_u64(3112244599778833558);
204			for val in rng.sample_iter(Standard).take(def.num_globals as usize) {
205				contract = contract
206					.global()
207					.value_type()
208					.i64()
209					.mutable()
210					.init_expr(Instruction::I64Const(val))
211					.build()
212			}
213		}
214
215		// Add function pointer table
216		if let Some(table) = def.table {
217			contract = contract
218				.table()
219				.with_min(table.num_elements)
220				.with_max(Some(table.num_elements))
221				.with_element(0, vec![table.function_index; table.num_elements as usize])
222				.build();
223		}
224
225		// Add the dummy section
226		if def.dummy_section > 0 {
227			contract = contract.with_section(Section::Custom(CustomSection::new(
228				"dummy".to_owned(),
229				vec![42; def.dummy_section as usize],
230			)));
231		}
232
233		let code = contract.build().into_bytes().unwrap();
234		let hash = T::Hashing::hash(&code);
235		Self { code: code.into(), hash }
236	}
237}
238
239impl<T: Config> WasmModule<T> {
240	/// Creates a wasm module with an empty `call` and `deploy` function and nothing else.
241	pub fn dummy() -> Self {
242		ModuleDefinition::default().into()
243	}
244
245	/// Same as `dummy` but with maximum sized linear memory and a dummy section of specified size.
246	pub fn dummy_with_bytes(dummy_bytes: u32) -> Self {
247		// We want the module to have the size `dummy_bytes`.
248		// This is not completely correct as the overhead grows when the contract grows
249		// because of variable length integer encoding. However, it is good enough to be that
250		// close for benchmarking purposes.
251		let module_overhead = 65;
252		ModuleDefinition {
253			memory: Some(ImportedMemory::max::<T>()),
254			dummy_section: dummy_bytes.saturating_sub(module_overhead),
255			..Default::default()
256		}
257		.into()
258	}
259
260	/// Creates a wasm module of `target_bytes` size. Used to benchmark the performance of
261	/// `instantiate_with_code` for different sizes of wasm modules. The generated module maximizes
262	/// instrumentation runtime by nesting blocks as deeply as possible given the byte budget.
263	/// `code_location`: Whether to place the code into `deploy` or `call`.
264	pub fn sized(target_bytes: u32, code_location: Location, use_float: bool) -> Self {
265		use self::elements::Instruction::{End, GetLocal, If, Return};
266		// Base size of a contract is 63 bytes and each expansion adds 6 bytes.
267		// We do one expansion less to account for the code section and function body
268		// size fields inside the binary wasm module representation which are leb128 encoded
269		// and therefore grow in size when the contract grows. We are not allowed to overshoot
270		// because of the maximum code size that is enforced by `instantiate_with_code`.
271		let mut expansions = (target_bytes.saturating_sub(63) / 6).saturating_sub(1);
272		const EXPANSION: [Instruction; 4] = [GetLocal(0), If(BlockType::NoResult), Return, End];
273		let mut locals = vec![Local::new(1, ValueType::I32)];
274		if use_float {
275			locals.push(Local::new(1, ValueType::F32));
276			locals.push(Local::new(2, ValueType::F32));
277			locals.push(Local::new(3, ValueType::F32));
278			expansions.saturating_dec();
279		}
280		let mut module =
281			ModuleDefinition { memory: Some(ImportedMemory::max::<T>()), ..Default::default() };
282		let body = Some(body::repeated_with_locals(&locals, expansions, &EXPANSION));
283		match code_location {
284			Location::Call => module.call_body = body,
285			Location::Deploy => module.deploy_body = body,
286		}
287		module.into()
288	}
289
290	/// Creates a wasm module that calls the imported function `noop` `repeat` times.
291	pub fn noop(repeat: u32) -> Self {
292		let pages = max_pages::<T>();
293		ModuleDefinition {
294			memory: Some(ImportedMemory::max::<T>()),
295			imported_functions: vec![ImportedFunction {
296				module: "seal0",
297				name: "noop",
298				params: vec![],
299				return_type: None,
300			}],
301			// Write the output buffer size. The output size will be overwritten by the
302			// supervisor with the real size when calling the getter. Since this size does not
303			// change between calls it suffices to start with an initial value and then just
304			// leave as whatever value was written there.
305			data_segments: vec![DataSegment {
306				offset: 0,
307				value: (pages * 64 * 1024 - 4).to_le_bytes().to_vec(),
308			}],
309			call_body: Some(body::repeated(
310				repeat,
311				&[
312					Instruction::Call(0), // call the imported function
313				],
314			)),
315			..Default::default()
316		}
317		.into()
318	}
319}
320
321/// Mechanisms to generate a function body that can be used inside a `ModuleDefinition`.
322pub mod body {
323	use super::*;
324
325	pub fn repeated(repetitions: u32, instructions: &[Instruction]) -> FuncBody {
326		repeated_with_locals(&[], repetitions, instructions)
327	}
328
329	pub fn repeated_with_locals(
330		locals: &[Local],
331		repetitions: u32,
332		instructions: &[Instruction],
333	) -> FuncBody {
334		let instructions = Instructions::new(
335			instructions
336				.iter()
337				.cycle()
338				.take(instructions.len() * usize::try_from(repetitions).unwrap())
339				.cloned()
340				.chain(core::iter::once(Instruction::End))
341				.collect(),
342		);
343		FuncBody::new(locals.to_vec(), instructions)
344	}
345
346	pub fn repeated_with_locals_using<const N: usize>(
347		locals: &[Local],
348		repetitions: u32,
349		mut f: impl FnMut() -> [Instruction; N],
350	) -> FuncBody {
351		let mut instructions = Vec::new();
352		for _ in 0..repetitions {
353			instructions.extend(f());
354		}
355		instructions.push(Instruction::End);
356		FuncBody::new(locals.to_vec(), Instructions::new(instructions))
357	}
358}
359
360/// The maximum amount of pages any contract is allowed to have according to the current `Schedule`.
361pub fn max_pages<T: Config>() -> u32 {
362	T::Schedule::get().limits.memory_pages
363}