referrerpolicy=no-referrer-when-downgrade

pallet_revive/
call_builder.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//! Types to build an environment that can be used to test and benchmark host function /
19//! pre-compiles.
20
21#![cfg(any(feature = "runtime-benchmarks", test))]
22// A lot of this code is only used in benchmarking but we also want to use the code for testing
23// pre-compiles eventually. For that we will probably export some of those types from the crate.
24// Until then we simply ignore the warnings that arise when compiling tests without runtime
25// benchmarks.
26#![cfg_attr(test, allow(dead_code))]
27
28use crate::{
29	address::AddressMapper,
30	exec::{ExportedFunction, Key, PrecompileExt, Stack},
31	limits,
32	metering::{TransactionLimits, TransactionMeter},
33	transient_storage::MeterEntry,
34	vm::pvm::{PreparedCall, Runtime},
35	AccountInfo, BalanceOf, BalanceWithDust, Code, CodeInfoOf, Config, ContractBlob, ContractInfo,
36	Error, ExecConfig, ExecOrigin as Origin, OriginFor, Pallet as Contracts, PristineCode, Weight,
37};
38use alloc::{vec, vec::Vec};
39use frame_support::{storage::child, traits::fungible::Mutate};
40use frame_system::RawOrigin;
41use pallet_revive_fixtures::bench as bench_fixtures;
42use sp_core::{H160, H256, U256};
43use sp_io::hashing::keccak_256;
44use sp_runtime::traits::{Bounded, Hash};
45
46type StackExt<'a, T> = Stack<'a, T, ContractBlob<T>>;
47
48/// A builder used to prepare a contract call.
49pub struct CallSetup<T: Config> {
50	contract: Contract<T>,
51	dest: T::AccountId,
52	origin: Origin<T>,
53	transaction_meter: TransactionMeter<T>,
54	value: BalanceOf<T>,
55	data: Vec<u8>,
56	transient_storage_size: u32,
57	exec_config: ExecConfig<T>,
58}
59
60impl<T> Default for CallSetup<T>
61where
62	T: Config,
63{
64	fn default() -> Self {
65		Self::new(VmBinaryModule::dummy())
66	}
67}
68
69impl<T> CallSetup<T>
70where
71	T: Config,
72{
73	/// Setup a new call for the given module.
74	pub fn new(module: VmBinaryModule) -> Self {
75		let contract = Contract::<T>::new(module, vec![]).unwrap();
76		let dest = contract.account_id.clone();
77		let origin = Origin::from_account_id(contract.caller.clone());
78
79		#[cfg(feature = "runtime-benchmarks")]
80		{
81			// Whitelist contract account, as it is already accounted for in the call benchmark
82			frame_benchmarking::benchmarking::add_to_whitelist(
83				frame_system::Account::<T>::hashed_key_for(&contract.account_id).into(),
84			);
85
86			// Whitelist the contract's contractInfo as it is already accounted for in the call
87			// benchmark
88			frame_benchmarking::benchmarking::add_to_whitelist(
89				crate::AccountInfoOf::<T>::hashed_key_for(&T::AddressMapper::to_address(
90					&contract.account_id,
91				))
92				.into(),
93			);
94		}
95
96		Self {
97			contract,
98			dest,
99			origin,
100			transaction_meter: TransactionMeter::new(TransactionLimits::WeightAndDeposit {
101				weight_limit: Weight::MAX,
102				deposit_limit: default_deposit_limit::<T>(),
103			})
104			.unwrap(),
105			value: 0u32.into(),
106			data: vec![],
107			transient_storage_size: 0,
108			exec_config: ExecConfig::new_substrate_tx(),
109		}
110	}
111
112	/// Set the meter's storage deposit limit.
113	pub fn set_storage_deposit_limit(&mut self, balance: BalanceOf<T>) {
114		self.transaction_meter = TransactionMeter::new(TransactionLimits::WeightAndDeposit {
115			weight_limit: Weight::MAX,
116			deposit_limit: balance,
117		})
118		.unwrap();
119	}
120
121	/// Set the call's origin.
122	pub fn set_origin(&mut self, origin: Origin<T>) {
123		self.origin = origin;
124	}
125
126	/// Set the contract's balance.
127	pub fn set_balance(&mut self, value: impl Into<BalanceWithDust<BalanceOf<T>>>) {
128		self.contract.set_balance(value);
129	}
130
131	/// Set the call's input data.
132	pub fn set_data(&mut self, value: Vec<u8>) {
133		self.data = value;
134	}
135
136	/// Set the transient storage size.
137	pub fn set_transient_storage_size(&mut self, size: u32) {
138		self.transient_storage_size = size;
139	}
140
141	/// Get the call's input data.
142	pub fn data(&self) -> Vec<u8> {
143		self.data.clone()
144	}
145
146	/// Get the call's contract.
147	pub fn contract(&self) -> Contract<T> {
148		self.contract.clone()
149	}
150
151	/// Build the call stack.
152	pub fn ext(&mut self) -> (StackExt<'_, T>, ContractBlob<T>) {
153		let mut ext = StackExt::bench_new_call(
154			T::AddressMapper::to_address(&self.dest),
155			self.origin.clone(),
156			&mut self.transaction_meter,
157			self.value,
158			&self.exec_config,
159		);
160		if self.transient_storage_size > 0 {
161			Self::with_transient_storage(&mut ext.0, self.transient_storage_size).unwrap();
162		}
163		ext
164	}
165
166	/// Prepare a call to the module.
167	pub fn prepare_call<'a>(
168		ext: &'a mut StackExt<'a, T>,
169		module: ContractBlob<T>,
170		input: Vec<u8>,
171		aux_data_size: u32,
172	) -> PreparedCall<'a, StackExt<'a, T>> {
173		module
174			.prepare_call(Runtime::new(ext, input), ExportedFunction::Call, aux_data_size)
175			.unwrap()
176	}
177
178	/// Add transient_storage
179	fn with_transient_storage(ext: &mut StackExt<T>, size: u32) -> Result<(), &'static str> {
180		let &MeterEntry { amount, limit } = ext.transient_storage().meter().current();
181		ext.transient_storage().meter().current_mut().limit = size;
182		for i in 1u32.. {
183			let mut key_data = i.to_le_bytes().to_vec();
184			while key_data.last() == Some(&0) {
185				key_data.pop();
186			}
187			let key = Key::try_from_var(key_data).unwrap();
188			if let Err(e) = ext.set_transient_storage(&key, Some(Vec::new()), false) {
189				// Restore previous settings.
190				ext.transient_storage().meter().current_mut().limit = limit;
191				ext.transient_storage().meter().current_mut().amount = amount;
192				if e == Error::<T>::OutOfTransientStorage.into() {
193					break;
194				} else {
195					return Err("Initialization of the transient storage failed");
196				}
197			}
198		}
199		Ok(())
200	}
201}
202
203/// The deposit limit we use for benchmarks.
204pub fn default_deposit_limit<T: Config>() -> BalanceOf<T> {
205	<BalanceOf<T>>::max_value()
206}
207
208/// The funding that each account that either calls or instantiates contracts is funded with.
209pub fn caller_funding<T: Config>() -> BalanceOf<T> {
210	// Minting can overflow, so we can't abuse of the funding. This value happens to be big enough,
211	// but not too big to make the total supply overflow.
212	BalanceOf::<T>::max_value() / 10_000u32.into()
213}
214
215/// An instantiated and deployed contract.
216#[derive(Clone)]
217pub struct Contract<T: Config> {
218	pub caller: T::AccountId,
219	pub account_id: T::AccountId,
220	pub address: H160,
221}
222
223impl<T> Contract<T>
224where
225	T: Config,
226{
227	/// Create new contract and use a default account id as instantiator.
228	pub fn new(module: VmBinaryModule, data: Vec<u8>) -> Result<Contract<T>, &'static str> {
229		let caller = T::AddressMapper::to_fallback_account_id(&crate::test_utils::ALICE_ADDR);
230		Self::with_caller(caller, module, data)
231	}
232
233	/// Create new contract and use an account id derived from the supplied index as instantiator.
234	#[cfg(feature = "runtime-benchmarks")]
235	pub fn with_index(
236		index: u32,
237		module: VmBinaryModule,
238		data: Vec<u8>,
239	) -> Result<Contract<T>, &'static str> {
240		Self::with_caller(frame_benchmarking::account("instantiator", index, 0), module, data)
241	}
242
243	/// Create new contract and use the supplied `caller` as instantiator.
244	pub fn with_caller(
245		caller: T::AccountId,
246		module: VmBinaryModule,
247		data: Vec<u8>,
248	) -> Result<Contract<T>, &'static str> {
249		T::Currency::set_balance(&caller, caller_funding::<T>());
250		let salt = Some([0xffu8; 32]);
251		let origin: OriginFor<T> = RawOrigin::Signed(caller.clone()).into();
252
253		// We ignore the error since we might also pass an already mapped account here.
254		Contracts::<T>::map_account(origin.clone()).ok();
255
256		#[cfg(feature = "runtime-benchmarks")]
257		frame_benchmarking::benchmarking::add_to_whitelist(
258			frame_system::Account::<T>::hashed_key_for(&caller).into(),
259		);
260
261		let outcome = Contracts::<T>::bare_instantiate(
262			origin,
263			U256::zero(),
264			TransactionLimits::WeightAndDeposit {
265				weight_limit: Weight::MAX,
266				deposit_limit: default_deposit_limit::<T>(),
267			},
268			Code::Upload(module.code),
269			data,
270			salt,
271			ExecConfig::new_substrate_tx(),
272		);
273
274		let address = outcome.result?.addr;
275		let account_id = T::AddressMapper::to_fallback_account_id(&address);
276		let result = Contract { caller, address, account_id };
277
278		AccountInfo::<T>::insert_contract(&address, result.info()?);
279		Ok(result)
280	}
281
282	/// Create a new contract with the supplied storage item count and size each.
283	pub fn with_storage(
284		code: VmBinaryModule,
285		stor_num: u32,
286		stor_size: u32,
287	) -> Result<Self, &'static str> {
288		let contract = Contract::<T>::new(code, vec![])?;
289		let storage_items = (0..stor_num)
290			.map(|i| {
291				let hash = T::Hashing::hash_of(&i)
292					.as_ref()
293					.try_into()
294					.map_err(|_| "Hash too big for storage key")?;
295				Ok((hash, vec![42u8; stor_size as usize]))
296			})
297			.collect::<Result<Vec<_>, &'static str>>()?;
298		contract.store(&storage_items)?;
299		Ok(contract)
300	}
301
302	/// Store the supplied storage items into this contracts storage.
303	pub fn store(&self, items: &Vec<([u8; 32], Vec<u8>)>) -> Result<(), &'static str> {
304		let info = self.info()?;
305		for item in items {
306			info.write(&Key::Fix(item.0), Some(item.1.clone()), None, false)
307				.map_err(|_| "Failed to write storage to restoration dest")?;
308		}
309
310		AccountInfo::<T>::insert_contract(&self.address, info);
311		Ok(())
312	}
313
314	/// Create a new contract with the specified unbalanced storage trie.
315	pub fn with_unbalanced_storage_trie(
316		code: VmBinaryModule,
317		key: &[u8],
318	) -> Result<Self, &'static str> {
319		/// Number of layers in a Radix16 unbalanced trie.
320		const UNBALANCED_TRIE_LAYERS: u32 = 20;
321
322		if (key.len() as u32) < UNBALANCED_TRIE_LAYERS.div_ceil(2) {
323			return Err("Key size too small to create the specified trie");
324		}
325
326		let value = vec![16u8; limits::STORAGE_BYTES as usize];
327		let contract = Contract::<T>::new(code, vec![])?;
328		let info = contract.info()?;
329		let child_trie_info = info.child_trie_info();
330		child::put_raw(&child_trie_info, &key, &value);
331		for l in 0..UNBALANCED_TRIE_LAYERS {
332			let pos = l as usize / 2;
333			let mut key_new = key.to_vec();
334			for i in 0u8..16 {
335				key_new[pos] = if l % 2 == 0 {
336					(key_new[pos] & 0xF0) | i
337				} else {
338					(key_new[pos] & 0x0F) | (i << 4)
339				};
340
341				if key == &key_new {
342					continue;
343				}
344				child::put_raw(&child_trie_info, &key_new, &value);
345			}
346		}
347		Ok(contract)
348	}
349
350	/// Get the `ContractInfo` of the `addr` or an error if it no longer exists.
351	pub fn address_info(addr: &T::AccountId) -> Result<ContractInfo<T>, &'static str> {
352		<AccountInfo<T>>::load_contract(&T::AddressMapper::to_address(addr))
353			.ok_or("Expected contract to exist at this point.")
354	}
355
356	/// Get the `ContractInfo` of this contract or an error if it no longer exists.
357	pub fn info(&self) -> Result<ContractInfo<T>, &'static str> {
358		Self::address_info(&self.account_id)
359	}
360
361	/// Set the balance of the contract to the supplied amount.
362	pub fn set_balance(&self, value: impl Into<BalanceWithDust<BalanceOf<T>>>) {
363		let (value, dust) = value.into().deconstruct();
364		T::Currency::set_balance(&self.account_id, value);
365		crate::AccountInfoOf::<T>::mutate(&self.address, |account| {
366			account.as_mut().map(|a| a.dust = dust);
367		});
368	}
369
370	/// Returns `true` iff all storage entries related to code storage exist.
371	pub fn code_exists(hash: &sp_core::H256) -> bool {
372		<PristineCode<T>>::contains_key(hash) && <CodeInfoOf<T>>::contains_key(&hash)
373	}
374
375	/// Returns `true` iff no storage entry related to code storage exist.
376	pub fn code_removed(hash: &sp_core::H256) -> bool {
377		!<PristineCode<T>>::contains_key(hash) && !<CodeInfoOf<T>>::contains_key(&hash)
378	}
379}
380
381/// A vm binary module ready to be put on chain.
382#[derive(Clone)]
383pub struct VmBinaryModule {
384	pub code: Vec<u8>,
385	pub hash: H256,
386}
387
388impl VmBinaryModule {
389	/// Return a contract code that does nothing.
390	pub fn dummy() -> Self {
391		Self::new(bench_fixtures::dummy().to_vec())
392	}
393
394	fn new(code: Vec<u8>) -> Self {
395		let hash = keccak_256(&code);
396		Self { code, hash: H256(hash) }
397	}
398}
399
400#[cfg(any(test, feature = "runtime-benchmarks"))]
401impl VmBinaryModule {
402	// Same as [`Self::sized`] but using EVM bytecode.
403	pub fn evm_sized(size: u32) -> Self {
404		use revm::bytecode::opcode::STOP;
405		let code = vec![STOP; size as usize];
406		Self::new(code)
407	}
408}
409
410#[cfg(feature = "runtime-benchmarks")]
411impl VmBinaryModule {
412	/// Same as [`Self::dummy`] but uses `replace_with` to make the code unique.
413	pub fn dummy_unique(replace_with: u32) -> Self {
414		Self::new(bench_fixtures::dummy_unique(replace_with))
415	}
416
417	/// Same as as `with_num_instructions` but based on the blob size.
418	///
419	/// This is needed when we weigh a blob without knowing how much instructions it
420	/// contains.
421	pub fn sized(size: u32) -> Self {
422		// Due to variable length encoding of instructions this is not precise. But we only
423		// need rough numbers for our benchmarks.
424		Self::with_num_instructions(size / 3)
425	}
426
427	/// A contract code of specified number of instructions that uses all its bytes for instructions
428	/// but will return immediately.
429	///
430	/// All the basic blocks are maximum sized (only the first is important though). This is to
431	/// account for the fact that the interpreter will compile one basic block at a time even
432	/// when no code is executed. Hence this contract will trigger the compilation of a maximum
433	/// sized basic block and then return with its first instruction.
434	///
435	/// All the code will be put into the "call" export. Hence this code can be safely used for the
436	/// `instantiate_with_code` benchmark where no compilation of any block should be measured.
437	pub fn with_num_instructions(num_instructions: u32) -> Self {
438		use alloc::{fmt::Write, string::ToString};
439		let mut text = "
440		pub @deploy:
441		ret
442		pub @call:
443		"
444		.to_string();
445		for i in 0..num_instructions {
446			match i {
447				// return execution right away without breaking up basic block
448				// SENTINEL is a hard coded syscall that terminates execution
449				0 => writeln!(text, "ecalli {}", crate::SENTINEL).unwrap(),
450				i if i % (limits::code::BASIC_BLOCK_SIZE - 1) == 0 =>
451					text.push_str("fallthrough\n"),
452				_ => text.push_str("a0 = a1 + a2\n"),
453			}
454		}
455		text.push_str("ret\n");
456		let code = polkavm_common::assembler::assemble(
457			Some(polkavm_common::program::InstructionSetKind::ReviveV1),
458			&text,
459		)
460		.unwrap();
461		Self::new(code)
462	}
463
464	/// A contract code that calls the "noop" host function in a loop depending in the input.
465	pub fn noop() -> Self {
466		Self::new(bench_fixtures::noop().to_vec())
467	}
468
469	/// A contract code that does unaligned memory accessed in a loop.
470	pub fn instr(do_load: bool) -> Self {
471		let load = match do_load {
472			false => "",
473			true => "a0 = u64 [a0]",
474		};
475		let text = alloc::format!(
476			"
477		pub @deploy:
478		ret
479		pub @call:
480			@loop:
481				jump @done if t0 == a1
482				{load}
483				t0 = t0 + 1
484				jump @loop
485			@done:
486		ret
487		"
488		);
489		let code = polkavm_common::assembler::assemble(
490			Some(polkavm_common::program::InstructionSetKind::ReviveV1),
491			&text,
492		)
493		.unwrap();
494		Self::new(code)
495	}
496
497	/// An evm contract that executes `size` JUMPDEST instructions.
498	pub fn evm_noop(size: u32) -> Self {
499		use revm::bytecode::opcode::JUMPDEST;
500
501		let code = vec![JUMPDEST; size as usize];
502		Self::new(code)
503	}
504}