#![cfg(any(feature = "runtime-benchmarks", test))]
#![cfg_attr(test, allow(dead_code))]
use crate::{
address::AddressMapper,
exec::{ExportedFunction, Key, PrecompileExt, Stack},
limits,
storage::meter::Meter,
transient_storage::MeterEntry,
wasm::{PreparedCall, Runtime},
BalanceOf, Code, CodeInfoOf, Config, ContractInfo, ContractInfoOf, DepositLimit, Error,
GasMeter, MomentOf, Origin, Pallet as Contracts, PristineCode, WasmBlob, Weight,
};
use alloc::{vec, vec::Vec};
use frame_support::{storage::child, traits::fungible::Mutate};
use frame_system::RawOrigin;
use pallet_revive_fixtures::bench as bench_fixtures;
use sp_core::{Get, H160, H256, U256};
use sp_io::hashing::keccak_256;
use sp_runtime::traits::{Bounded, Hash};
type StackExt<'a, T> = Stack<'a, T, WasmBlob<T>>;
pub struct CallSetup<T: Config> {
contract: Contract<T>,
dest: T::AccountId,
origin: Origin<T>,
gas_meter: GasMeter<T>,
storage_meter: Meter<T>,
value: BalanceOf<T>,
data: Vec<u8>,
transient_storage_size: u32,
}
impl<T> Default for CallSetup<T>
where
T: Config,
BalanceOf<T>: Into<U256> + TryFrom<U256>,
MomentOf<T>: Into<U256>,
T::Hash: frame_support::traits::IsType<H256>,
{
fn default() -> Self {
Self::new(WasmModule::dummy())
}
}
impl<T> CallSetup<T>
where
T: Config,
BalanceOf<T>: Into<U256> + TryFrom<U256>,
MomentOf<T>: Into<U256>,
T::Hash: frame_support::traits::IsType<H256>,
{
pub fn new(module: WasmModule) -> Self {
let contract = Contract::<T>::new(module, vec![]).unwrap();
let dest = contract.account_id.clone();
let origin = Origin::from_account_id(contract.caller.clone());
let storage_meter = Meter::new(default_deposit_limit::<T>());
#[cfg(feature = "runtime-benchmarks")]
{
frame_benchmarking::benchmarking::add_to_whitelist(
frame_system::Account::<T>::hashed_key_for(&contract.account_id).into(),
);
frame_benchmarking::benchmarking::add_to_whitelist(
crate::ContractInfoOf::<T>::hashed_key_for(&T::AddressMapper::to_address(
&contract.account_id,
))
.into(),
);
}
Self {
contract,
dest,
origin,
gas_meter: GasMeter::new(Weight::MAX),
storage_meter,
value: 0u32.into(),
data: vec![],
transient_storage_size: 0,
}
}
pub fn set_storage_deposit_limit(&mut self, balance: BalanceOf<T>) {
self.storage_meter = Meter::new(balance);
}
pub fn set_origin(&mut self, origin: Origin<T>) {
self.origin = origin;
}
pub fn set_balance(&mut self, value: BalanceOf<T>) {
self.contract.set_balance(value);
}
pub fn set_data(&mut self, value: Vec<u8>) {
self.data = value;
}
pub fn set_transient_storage_size(&mut self, size: u32) {
self.transient_storage_size = size;
}
pub fn data(&self) -> Vec<u8> {
self.data.clone()
}
pub fn contract(&self) -> Contract<T> {
self.contract.clone()
}
pub fn ext(&mut self) -> (StackExt<'_, T>, WasmBlob<T>) {
let mut ext = StackExt::bench_new_call(
T::AddressMapper::to_address(&self.dest),
self.origin.clone(),
&mut self.gas_meter,
&mut self.storage_meter,
self.value,
);
if self.transient_storage_size > 0 {
Self::with_transient_storage(&mut ext.0, self.transient_storage_size).unwrap();
}
ext
}
pub fn prepare_call<'a>(
ext: &'a mut StackExt<'a, T>,
module: WasmBlob<T>,
input: Vec<u8>,
aux_data_size: u32,
) -> PreparedCall<'a, StackExt<'a, T>> {
module
.prepare_call(Runtime::new(ext, input), ExportedFunction::Call, aux_data_size)
.unwrap()
}
fn with_transient_storage(ext: &mut StackExt<T>, size: u32) -> Result<(), &'static str> {
let &MeterEntry { amount, limit } = ext.transient_storage().meter().current();
ext.transient_storage().meter().current_mut().limit = size;
for i in 1u32.. {
let mut key_data = i.to_le_bytes().to_vec();
while key_data.last() == Some(&0) {
key_data.pop();
}
let key = Key::try_from_var(key_data).unwrap();
if let Err(e) = ext.set_transient_storage(&key, Some(Vec::new()), false) {
ext.transient_storage().meter().current_mut().limit = limit;
ext.transient_storage().meter().current_mut().amount = amount;
if e == Error::<T>::OutOfTransientStorage.into() {
break;
} else {
return Err("Initialization of the transient storage failed");
}
}
}
Ok(())
}
}
pub fn default_deposit_limit<T: Config>() -> BalanceOf<T> {
(T::DepositPerByte::get() * 1024u32.into() * 1024u32.into()) +
T::DepositPerItem::get() * 1024u32.into()
}
pub fn caller_funding<T: Config>() -> BalanceOf<T> {
BalanceOf::<T>::max_value() / 10_000u32.into()
}
#[derive(Clone)]
pub struct Contract<T: Config> {
pub caller: T::AccountId,
pub account_id: T::AccountId,
pub address: H160,
}
impl<T> Contract<T>
where
T: Config,
BalanceOf<T>: Into<U256> + TryFrom<U256>,
MomentOf<T>: Into<U256>,
T::Hash: frame_support::traits::IsType<H256>,
{
pub fn new(module: WasmModule, data: Vec<u8>) -> Result<Contract<T>, &'static str> {
let caller = T::AddressMapper::to_fallback_account_id(&crate::test_utils::ALICE_ADDR);
Self::with_caller(caller, module, data)
}
#[cfg(feature = "runtime-benchmarks")]
pub fn with_index(
index: u32,
module: WasmModule,
data: Vec<u8>,
) -> Result<Contract<T>, &'static str> {
Self::with_caller(frame_benchmarking::account("instantiator", index, 0), module, data)
}
pub fn with_caller(
caller: T::AccountId,
module: WasmModule,
data: Vec<u8>,
) -> Result<Contract<T>, &'static str> {
T::Currency::set_balance(&caller, caller_funding::<T>());
let salt = Some([0xffu8; 32]);
let origin: T::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into();
Contracts::<T>::map_account(origin.clone()).ok();
#[cfg(feature = "runtime-benchmarks")]
frame_benchmarking::benchmarking::add_to_whitelist(
frame_system::Account::<T>::hashed_key_for(&caller).into(),
);
let outcome = Contracts::<T>::bare_instantiate(
origin,
0u32.into(),
Weight::MAX,
DepositLimit::Balance(default_deposit_limit::<T>()),
Code::Upload(module.code),
data,
salt,
);
let address = outcome.result?.addr;
let account_id = T::AddressMapper::to_fallback_account_id(&address);
let result = Contract { caller, address, account_id };
ContractInfoOf::<T>::insert(&address, result.info()?);
Ok(result)
}
pub fn with_storage(
code: WasmModule,
stor_num: u32,
stor_size: u32,
) -> Result<Self, &'static str> {
let contract = Contract::<T>::new(code, vec![])?;
let storage_items = (0..stor_num)
.map(|i| {
let hash = T::Hashing::hash_of(&i)
.as_ref()
.try_into()
.map_err(|_| "Hash too big for storage key")?;
Ok((hash, vec![42u8; stor_size as usize]))
})
.collect::<Result<Vec<_>, &'static str>>()?;
contract.store(&storage_items)?;
Ok(contract)
}
pub fn store(&self, items: &Vec<([u8; 32], Vec<u8>)>) -> Result<(), &'static str> {
let info = self.info()?;
for item in items {
info.write(&Key::Fix(item.0), Some(item.1.clone()), None, false)
.map_err(|_| "Failed to write storage to restoration dest")?;
}
<ContractInfoOf<T>>::insert(&self.address, info);
Ok(())
}
pub fn with_unbalanced_storage_trie(
code: WasmModule,
key: &[u8],
) -> Result<Self, &'static str> {
const UNBALANCED_TRIE_LAYERS: u32 = 20;
if (key.len() as u32) < (UNBALANCED_TRIE_LAYERS + 1) / 2 {
return Err("Key size too small to create the specified trie");
}
let value = vec![16u8; limits::PAYLOAD_BYTES as usize];
let contract = Contract::<T>::new(code, vec![])?;
let info = contract.info()?;
let child_trie_info = info.child_trie_info();
child::put_raw(&child_trie_info, &key, &value);
for l in 0..UNBALANCED_TRIE_LAYERS {
let pos = l as usize / 2;
let mut key_new = key.to_vec();
for i in 0u8..16 {
key_new[pos] = if l % 2 == 0 {
(key_new[pos] & 0xF0) | i
} else {
(key_new[pos] & 0x0F) | (i << 4)
};
if key == &key_new {
continue;
}
child::put_raw(&child_trie_info, &key_new, &value);
}
}
Ok(contract)
}
pub fn address_info(addr: &T::AccountId) -> Result<ContractInfo<T>, &'static str> {
ContractInfoOf::<T>::get(T::AddressMapper::to_address(addr))
.ok_or("Expected contract to exist at this point.")
}
pub fn info(&self) -> Result<ContractInfo<T>, &'static str> {
Self::address_info(&self.account_id)
}
pub fn set_balance(&self, balance: BalanceOf<T>) {
T::Currency::set_balance(&self.account_id, balance);
}
pub fn code_exists(hash: &sp_core::H256) -> bool {
<PristineCode<T>>::contains_key(hash) && <CodeInfoOf<T>>::contains_key(&hash)
}
pub fn code_removed(hash: &sp_core::H256) -> bool {
!<PristineCode<T>>::contains_key(hash) && !<CodeInfoOf<T>>::contains_key(&hash)
}
}
#[derive(Clone)]
pub struct WasmModule {
pub code: Vec<u8>,
pub hash: H256,
}
impl WasmModule {
pub fn dummy() -> Self {
Self::new(bench_fixtures::DUMMY.to_vec())
}
fn new(code: Vec<u8>) -> Self {
let hash = keccak_256(&code);
Self { code, hash: H256(hash) }
}
}
#[cfg(feature = "runtime-benchmarks")]
impl WasmModule {
pub fn dummy_unique(replace_with: u32) -> Self {
Self::new(bench_fixtures::dummy_unique(replace_with))
}
pub fn sized(size: u32) -> Self {
Self::with_num_instructions(size / 3)
}
pub fn with_num_instructions(num_instructions: u32) -> Self {
use alloc::{fmt::Write, string::ToString};
let mut text = "
pub @deploy:
ret
pub @call:
"
.to_string();
for i in 0..num_instructions {
match i {
0 => writeln!(text, "ecalli {}", crate::SENTINEL).unwrap(),
i if i % (limits::code::BASIC_BLOCK_SIZE - 1) == 0 =>
text.push_str("fallthrough\n"),
_ => text.push_str("a0 = a1 + a2\n"),
}
}
text.push_str("ret\n");
let code = polkavm_common::assembler::assemble(&text).unwrap();
Self::new(code)
}
pub fn noop() -> Self {
Self::new(bench_fixtures::NOOP.to_vec())
}
pub fn instr(do_load: bool) -> Self {
let load = match do_load {
false => "",
true => "a0 = u64 [a0]",
};
let text = alloc::format!(
"
pub @deploy:
ret
pub @call:
@loop:
jump @done if t0 == a1
{load}
t0 = t0 + 1
jump @loop
@done:
ret
"
);
let code = polkavm_common::assembler::assemble(&text).unwrap();
Self::new(code)
}
}