feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Functions that deal with address derivation.
|
||||
|
||||
use crate::{CodeHash, Config};
|
||||
use codec::{Decode, Encode};
|
||||
use pezsp_runtime::traits::{Hash, TrailingZeroInput};
|
||||
|
||||
/// Provides the contract address generation method.
|
||||
///
|
||||
/// See [`DefaultAddressGenerator`] for the default implementation.
|
||||
///
|
||||
/// # Note for implementors
|
||||
///
|
||||
/// 1. Make sure that there are no collisions, different inputs never lead to the same output.
|
||||
/// 2. Make sure that the same inputs lead to the same output.
|
||||
pub trait AddressGenerator<T: Config> {
|
||||
/// The address of a contract based on the given instantiate parameters.
|
||||
///
|
||||
/// Changing the formular for an already deployed chain is fine as long as no collisions
|
||||
/// with the old formular. Changes only affect existing contracts.
|
||||
fn contract_address(
|
||||
deploying_address: &T::AccountId,
|
||||
code_hash: &CodeHash<T>,
|
||||
input_data: &[u8],
|
||||
salt: &[u8],
|
||||
) -> T::AccountId;
|
||||
}
|
||||
|
||||
/// Default address generator.
|
||||
///
|
||||
/// This is the default address generator used by contract instantiation. Its result
|
||||
/// is only dependent on its inputs. It can therefore be used to reliably predict the
|
||||
/// address of a contract. This is akin to the formula of eth's CREATE2 opcode. There
|
||||
/// is no CREATE equivalent because CREATE2 is strictly more powerful.
|
||||
/// Formula:
|
||||
/// `hash("contract_addr_v1" ++ deploying_address ++ code_hash ++ input_data ++ salt)`
|
||||
pub struct DefaultAddressGenerator;
|
||||
|
||||
impl<T: Config> AddressGenerator<T> for DefaultAddressGenerator {
|
||||
/// Formula: `hash("contract_addr_v1" ++ deploying_address ++ code_hash ++ input_data ++ salt)`
|
||||
fn contract_address(
|
||||
deploying_address: &T::AccountId,
|
||||
code_hash: &CodeHash<T>,
|
||||
input_data: &[u8],
|
||||
salt: &[u8],
|
||||
) -> T::AccountId {
|
||||
let entropy = (b"contract_addr_v1", deploying_address, code_hash, input_data, salt)
|
||||
.using_encoded(T::Hashing::hash);
|
||||
Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
|
||||
.expect("infinite length input; no invalid inputs for type; qed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{
|
||||
benchmarking::{Contract, WasmModule},
|
||||
exec::{Ext, Key, Stack},
|
||||
storage::meter::Meter,
|
||||
transient_storage::MeterEntry,
|
||||
wasm::Runtime,
|
||||
BalanceOf, Config, DebugBufferVec, Determinism, Error, ExecReturnValue, GasMeter, Origin,
|
||||
Schedule, TypeInfo, WasmBlob, Weight,
|
||||
};
|
||||
use alloc::{vec, vec::Vec};
|
||||
use codec::{Encode, HasCompact};
|
||||
use core::fmt::Debug;
|
||||
use pezframe_benchmarking::benchmarking;
|
||||
use pezsp_core::Get;
|
||||
|
||||
type StackExt<'a, T> = Stack<'a, T, WasmBlob<T>>;
|
||||
|
||||
/// A prepared contract call ready to be executed.
|
||||
pub struct PreparedCall<'a, T: Config> {
|
||||
func: wasmi::Func,
|
||||
store: wasmi::Store<Runtime<'a, StackExt<'a, T>>>,
|
||||
}
|
||||
|
||||
impl<'a, T: Config> PreparedCall<'a, T> {
|
||||
pub fn call(mut self) -> ExecReturnValue {
|
||||
let result = self.func.call(&mut self.store, &[], &mut []);
|
||||
WasmBlob::<T>::process_result(self.store, result).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder used to prepare a contract call.
|
||||
pub struct CallSetup<T: Config> {
|
||||
contract: Contract<T>,
|
||||
dest: T::AccountId,
|
||||
origin: Origin<T>,
|
||||
gas_meter: GasMeter<T>,
|
||||
storage_meter: Meter<T>,
|
||||
schedule: Schedule<T>,
|
||||
value: BalanceOf<T>,
|
||||
debug_message: Option<DebugBufferVec<T>>,
|
||||
determinism: Determinism,
|
||||
data: Vec<u8>,
|
||||
transient_storage_size: u32,
|
||||
}
|
||||
|
||||
impl<T> Default for CallSetup<T>
|
||||
where
|
||||
T: Config + pezpallet_balances::Config,
|
||||
<BalanceOf<T> as HasCompact>::Type: Clone + Eq + PartialEq + Debug + TypeInfo + Encode,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new(WasmModule::dummy())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CallSetup<T>
|
||||
where
|
||||
T: Config + pezpallet_balances::Config,
|
||||
<BalanceOf<T> as HasCompact>::Type: Clone + Eq + PartialEq + Debug + TypeInfo + Encode,
|
||||
{
|
||||
/// Setup a new call for the given module.
|
||||
pub fn new(module: WasmModule<T>) -> Self {
|
||||
let contract = Contract::<T>::new(module.clone(), vec![]).unwrap();
|
||||
let dest = contract.account_id.clone();
|
||||
let origin = Origin::from_account_id(contract.caller.clone());
|
||||
|
||||
let storage_meter = Meter::new(&origin, None, 0u32.into()).unwrap();
|
||||
|
||||
// Whitelist contract account, as it is already accounted for in the call benchmark
|
||||
benchmarking::add_to_whitelist(
|
||||
pezframe_system::Account::<T>::hashed_key_for(&contract.account_id).into(),
|
||||
);
|
||||
|
||||
// Whitelist the contract's contractInfo as it is already accounted for in the call
|
||||
// benchmark
|
||||
benchmarking::add_to_whitelist(
|
||||
crate::ContractInfoOf::<T>::hashed_key_for(&contract.account_id).into(),
|
||||
);
|
||||
|
||||
Self {
|
||||
contract,
|
||||
dest,
|
||||
origin,
|
||||
gas_meter: GasMeter::new(Weight::MAX),
|
||||
storage_meter,
|
||||
schedule: T::Schedule::get(),
|
||||
value: 0u32.into(),
|
||||
debug_message: None,
|
||||
determinism: Determinism::Enforced,
|
||||
data: vec![],
|
||||
transient_storage_size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the meter's storage deposit limit.
|
||||
pub fn set_storage_deposit_limit(&mut self, balance: BalanceOf<T>) {
|
||||
self.storage_meter = Meter::new(&self.origin, Some(balance), 0u32.into()).unwrap();
|
||||
}
|
||||
|
||||
/// Set the call's origin.
|
||||
pub fn set_origin(&mut self, origin: Origin<T>) {
|
||||
self.origin = origin;
|
||||
}
|
||||
|
||||
/// Set the contract's balance.
|
||||
pub fn set_balance(&mut self, value: BalanceOf<T>) {
|
||||
self.contract.set_balance(value);
|
||||
}
|
||||
|
||||
/// Set the call's input data.
|
||||
pub fn set_data(&mut self, value: Vec<u8>) {
|
||||
self.data = value;
|
||||
}
|
||||
|
||||
/// Set the transient storage size.
|
||||
pub fn set_transient_storage_size(&mut self, size: u32) {
|
||||
self.transient_storage_size = size;
|
||||
}
|
||||
|
||||
/// Set the debug message.
|
||||
pub fn enable_debug_message(&mut self) {
|
||||
self.debug_message = Some(Default::default());
|
||||
}
|
||||
|
||||
/// Get the debug message.
|
||||
pub fn debug_message(&self) -> Option<DebugBufferVec<T>> {
|
||||
self.debug_message.clone()
|
||||
}
|
||||
|
||||
/// Get the call's input data.
|
||||
pub fn data(&self) -> Vec<u8> {
|
||||
self.data.clone()
|
||||
}
|
||||
|
||||
/// Get the call's contract.
|
||||
pub fn contract(&self) -> Contract<T> {
|
||||
self.contract.clone()
|
||||
}
|
||||
|
||||
/// Build the call stack.
|
||||
pub fn ext(&mut self) -> (StackExt<'_, T>, WasmBlob<T>) {
|
||||
let mut ext = StackExt::bench_new_call(
|
||||
self.dest.clone(),
|
||||
self.origin.clone(),
|
||||
&mut self.gas_meter,
|
||||
&mut self.storage_meter,
|
||||
&self.schedule,
|
||||
self.value,
|
||||
self.debug_message.as_mut(),
|
||||
self.determinism,
|
||||
);
|
||||
if self.transient_storage_size > 0 {
|
||||
Self::with_transient_storage(&mut ext.0, self.transient_storage_size).unwrap();
|
||||
}
|
||||
ext
|
||||
}
|
||||
|
||||
/// Prepare a call to the module.
|
||||
pub fn prepare_call<'a>(
|
||||
ext: &'a mut StackExt<'a, T>,
|
||||
module: WasmBlob<T>,
|
||||
input: Vec<u8>,
|
||||
) -> PreparedCall<'a, T> {
|
||||
let (func, store) = module.bench_prepare_call(ext, input);
|
||||
PreparedCall { func, store }
|
||||
}
|
||||
|
||||
/// Add transient_storage
|
||||
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::<T>::try_from_var(key_data).unwrap();
|
||||
if let Err(e) = ext.set_transient_storage(&key, Some(Vec::new()), false) {
|
||||
// Restore previous settings.
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! memory(
|
||||
($($bytes:expr,)*) => {
|
||||
vec![]
|
||||
.into_iter()
|
||||
$(.chain($bytes))*
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! build_runtime(
|
||||
($runtime:ident, $memory:ident: [$($segment:expr,)*]) => {
|
||||
$crate::build_runtime!($runtime, _contract, $memory: [$($segment,)*]);
|
||||
};
|
||||
($runtime:ident, $contract:ident, $memory:ident: [$($bytes:expr,)*]) => {
|
||||
$crate::build_runtime!($runtime, $contract);
|
||||
let mut $memory = $crate::memory!($($bytes,)*);
|
||||
};
|
||||
($runtime:ident, $contract:ident) => {
|
||||
let mut setup = CallSetup::<T>::default();
|
||||
let $contract = setup.contract();
|
||||
let input = setup.data();
|
||||
let (mut ext, _) = setup.ext();
|
||||
let mut $runtime = crate::wasm::Runtime::new(&mut ext, input);
|
||||
};
|
||||
);
|
||||
@@ -0,0 +1,363 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Functions to procedurally construct contract code used for benchmarking.
|
||||
//!
|
||||
//! In order to be able to benchmark events that are triggered by contract execution
|
||||
//! (API calls into seal, individual instructions), we need to generate contracts that
|
||||
//! perform those events. Because those contracts can get very big we cannot simply define
|
||||
//! them as text (.wat) as this will be too slow and consume too much memory. Therefore
|
||||
//! we define this simple definition of a contract that can be passed to `create_code` that
|
||||
//! compiles it down into a `WasmModule` that can be used as a contract's code.
|
||||
|
||||
use crate::Config;
|
||||
use alloc::{borrow::ToOwned, vec, vec::Vec};
|
||||
use pezframe_support::traits::Get;
|
||||
use pezsp_runtime::{traits::Hash, Saturating};
|
||||
use wasm_instrument::parity_wasm::{
|
||||
builder,
|
||||
elements::{
|
||||
self, BlockType, CustomSection, FuncBody, Instruction, Instructions, Local, Section,
|
||||
ValueType,
|
||||
},
|
||||
};
|
||||
|
||||
/// The location where to put the generated code.
|
||||
pub enum Location {
|
||||
/// Generate all code into the `call` exported function.
|
||||
Call,
|
||||
/// Generate all code into the `deploy` exported function.
|
||||
Deploy,
|
||||
}
|
||||
|
||||
/// Pass to `create_code` in order to create a compiled `WasmModule`.
|
||||
///
|
||||
/// This exists to have a more declarative way to describe a wasm module than to use
|
||||
/// parity-wasm directly. It is tailored to fit the structure of contracts that are
|
||||
/// needed for benchmarking.
|
||||
#[derive(Default)]
|
||||
pub struct ModuleDefinition {
|
||||
/// Imported memory attached to the module. No memory is imported if `None`.
|
||||
pub memory: Option<ImportedMemory>,
|
||||
/// Initializers for the imported memory.
|
||||
pub data_segments: Vec<DataSegment>,
|
||||
/// Creates the supplied amount of i64 mutable globals initialized with random values.
|
||||
pub num_globals: u32,
|
||||
/// List of functions that the module should import. They start with index 0.
|
||||
pub imported_functions: Vec<ImportedFunction>,
|
||||
/// Function body of the exported `deploy` function. Body is empty if `None`.
|
||||
/// Its index is `imported_functions.len()`.
|
||||
pub deploy_body: Option<FuncBody>,
|
||||
/// Function body of the exported `call` function. Body is empty if `None`.
|
||||
/// Its index is `imported_functions.len() + 1`.
|
||||
pub call_body: Option<FuncBody>,
|
||||
/// Function body of a non-exported function with index `imported_functions.len() + 2`.
|
||||
pub aux_body: Option<FuncBody>,
|
||||
/// The amount of I64 arguments the aux function should have.
|
||||
pub aux_arg_num: u32,
|
||||
/// Create a table containing function pointers.
|
||||
pub table: Option<TableSegment>,
|
||||
/// Create a section named "dummy" of the specified size. This is useful in order to
|
||||
/// benchmark the overhead of loading and storing codes of specified sizes. The dummy
|
||||
/// section only contributes to the size of the contract but does not affect execution.
|
||||
pub dummy_section: u32,
|
||||
}
|
||||
|
||||
pub struct TableSegment {
|
||||
/// How many elements should be created inside the table.
|
||||
pub num_elements: u32,
|
||||
/// The function index with which all table elements should be initialized.
|
||||
pub function_index: u32,
|
||||
}
|
||||
|
||||
pub struct DataSegment {
|
||||
pub offset: u32,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImportedMemory {
|
||||
pub min_pages: u32,
|
||||
pub max_pages: u32,
|
||||
}
|
||||
|
||||
impl ImportedMemory {
|
||||
pub fn max<T: Config>() -> Self {
|
||||
let pages = max_pages::<T>();
|
||||
Self { min_pages: pages, max_pages: pages }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImportedFunction {
|
||||
pub module: &'static str,
|
||||
pub name: &'static str,
|
||||
pub params: Vec<ValueType>,
|
||||
pub return_type: Option<ValueType>,
|
||||
}
|
||||
|
||||
/// A wasm module ready to be put on chain.
|
||||
#[derive(Clone)]
|
||||
pub struct WasmModule<T: Config> {
|
||||
pub code: Vec<u8>,
|
||||
pub hash: <T::Hashing as Hash>::Output,
|
||||
}
|
||||
|
||||
impl<T: Config> From<ModuleDefinition> for WasmModule<T> {
|
||||
fn from(def: ModuleDefinition) -> Self {
|
||||
// internal functions start at that offset.
|
||||
let func_offset = u32::try_from(def.imported_functions.len()).unwrap();
|
||||
|
||||
// Every contract must export "deploy" and "call" functions.
|
||||
let mut contract = builder::module()
|
||||
// deploy function (first internal function)
|
||||
.function()
|
||||
.signature()
|
||||
.build()
|
||||
.with_body(
|
||||
def.deploy_body
|
||||
.unwrap_or_else(|| FuncBody::new(Vec::new(), Instructions::empty())),
|
||||
)
|
||||
.build()
|
||||
// call function (second internal function)
|
||||
.function()
|
||||
.signature()
|
||||
.build()
|
||||
.with_body(
|
||||
def.call_body
|
||||
.unwrap_or_else(|| FuncBody::new(Vec::new(), Instructions::empty())),
|
||||
)
|
||||
.build()
|
||||
.export()
|
||||
.field("deploy")
|
||||
.internal()
|
||||
.func(func_offset)
|
||||
.build()
|
||||
.export()
|
||||
.field("call")
|
||||
.internal()
|
||||
.func(func_offset + 1)
|
||||
.build();
|
||||
|
||||
// If specified we add an additional internal function
|
||||
if let Some(body) = def.aux_body {
|
||||
let mut signature = contract.function().signature();
|
||||
for _ in 0..def.aux_arg_num {
|
||||
signature = signature.with_param(ValueType::I64);
|
||||
}
|
||||
contract = signature.build().with_body(body).build();
|
||||
}
|
||||
|
||||
// Grant access to linear memory.
|
||||
// Every contract module is required to have an imported memory.
|
||||
// If no memory is specified in the passed ModuleDefinition, then
|
||||
// default to (1, 1).
|
||||
let (init, max) = if let Some(memory) = &def.memory {
|
||||
(memory.min_pages, Some(memory.max_pages))
|
||||
} else {
|
||||
(1, Some(1))
|
||||
};
|
||||
|
||||
contract = contract.import().path("env", "memory").external().memory(init, max).build();
|
||||
|
||||
// Import supervisor functions. They start with idx 0.
|
||||
for func in def.imported_functions {
|
||||
let sig = builder::signature()
|
||||
.with_params(func.params)
|
||||
.with_results(func.return_type)
|
||||
.build_sig();
|
||||
let sig = contract.push_signature(sig);
|
||||
contract = contract
|
||||
.import()
|
||||
.module(func.module)
|
||||
.field(func.name)
|
||||
.with_external(elements::External::Function(sig))
|
||||
.build();
|
||||
}
|
||||
|
||||
// Initialize memory
|
||||
for data in def.data_segments {
|
||||
contract = contract
|
||||
.data()
|
||||
.offset(Instruction::I32Const(data.offset as i32))
|
||||
.value(data.value)
|
||||
.build()
|
||||
}
|
||||
|
||||
// Add global variables
|
||||
if def.num_globals > 0 {
|
||||
use rand::{distributions::Standard, prelude::*};
|
||||
let rng = rand_pcg::Pcg32::seed_from_u64(3112244599778833558);
|
||||
for val in rng.sample_iter(Standard).take(def.num_globals as usize) {
|
||||
contract = contract
|
||||
.global()
|
||||
.value_type()
|
||||
.i64()
|
||||
.mutable()
|
||||
.init_expr(Instruction::I64Const(val))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
// Add function pointer table
|
||||
if let Some(table) = def.table {
|
||||
contract = contract
|
||||
.table()
|
||||
.with_min(table.num_elements)
|
||||
.with_max(Some(table.num_elements))
|
||||
.with_element(0, vec![table.function_index; table.num_elements as usize])
|
||||
.build();
|
||||
}
|
||||
|
||||
// Add the dummy section
|
||||
if def.dummy_section > 0 {
|
||||
contract = contract.with_section(Section::Custom(CustomSection::new(
|
||||
"dummy".to_owned(),
|
||||
vec![42; def.dummy_section as usize],
|
||||
)));
|
||||
}
|
||||
|
||||
let code = contract.build().into_bytes().unwrap();
|
||||
let hash = T::Hashing::hash(&code);
|
||||
Self { code: code.into(), hash }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> WasmModule<T> {
|
||||
/// Creates a wasm module with an empty `call` and `deploy` function and nothing else.
|
||||
pub fn dummy() -> Self {
|
||||
ModuleDefinition::default().into()
|
||||
}
|
||||
|
||||
/// Same as `dummy` but with maximum sized linear memory and a dummy section of specified size.
|
||||
pub fn dummy_with_bytes(dummy_bytes: u32) -> Self {
|
||||
// We want the module to have the size `dummy_bytes`.
|
||||
// This is not completely correct as the overhead grows when the contract grows
|
||||
// because of variable length integer encoding. However, it is good enough to be that
|
||||
// close for benchmarking purposes.
|
||||
let module_overhead = 65;
|
||||
ModuleDefinition {
|
||||
memory: Some(ImportedMemory::max::<T>()),
|
||||
dummy_section: dummy_bytes.saturating_sub(module_overhead),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Creates a wasm module of `target_bytes` size. Used to benchmark the performance of
|
||||
/// `instantiate_with_code` for different sizes of wasm modules. The generated module maximizes
|
||||
/// instrumentation runtime by nesting blocks as deeply as possible given the byte budget.
|
||||
/// `code_location`: Whether to place the code into `deploy` or `call`.
|
||||
pub fn sized(target_bytes: u32, code_location: Location, use_float: bool) -> Self {
|
||||
use self::elements::Instruction::{End, GetLocal, If, Return};
|
||||
// Base size of a contract is 63 bytes and each expansion adds 6 bytes.
|
||||
// We do one expansion less to account for the code section and function body
|
||||
// size fields inside the binary wasm module representation which are leb128 encoded
|
||||
// and therefore grow in size when the contract grows. We are not allowed to overshoot
|
||||
// because of the maximum code size that is enforced by `instantiate_with_code`.
|
||||
let mut expansions = (target_bytes.saturating_sub(63) / 6).saturating_sub(1);
|
||||
const EXPANSION: [Instruction; 4] = [GetLocal(0), If(BlockType::NoResult), Return, End];
|
||||
let mut locals = vec![Local::new(1, ValueType::I32)];
|
||||
if use_float {
|
||||
locals.push(Local::new(1, ValueType::F32));
|
||||
locals.push(Local::new(2, ValueType::F32));
|
||||
locals.push(Local::new(3, ValueType::F32));
|
||||
expansions.saturating_dec();
|
||||
}
|
||||
let mut module =
|
||||
ModuleDefinition { memory: Some(ImportedMemory::max::<T>()), ..Default::default() };
|
||||
let body = Some(body::repeated_with_locals(&locals, expansions, &EXPANSION));
|
||||
match code_location {
|
||||
Location::Call => module.call_body = body,
|
||||
Location::Deploy => module.deploy_body = body,
|
||||
}
|
||||
module.into()
|
||||
}
|
||||
|
||||
/// Creates a wasm module that calls the imported function `noop` `repeat` times.
|
||||
pub fn noop(repeat: u32) -> Self {
|
||||
let pages = max_pages::<T>();
|
||||
ModuleDefinition {
|
||||
memory: Some(ImportedMemory::max::<T>()),
|
||||
imported_functions: vec![ImportedFunction {
|
||||
module: "seal0",
|
||||
name: "noop",
|
||||
params: vec![],
|
||||
return_type: None,
|
||||
}],
|
||||
// Write the output buffer size. The output size will be overwritten by the
|
||||
// supervisor with the real size when calling the getter. Since this size does not
|
||||
// change between calls it suffices to start with an initial value and then just
|
||||
// leave as whatever value was written there.
|
||||
data_segments: vec![DataSegment {
|
||||
offset: 0,
|
||||
value: (pages * 64 * 1024 - 4).to_le_bytes().to_vec(),
|
||||
}],
|
||||
call_body: Some(body::repeated(
|
||||
repeat,
|
||||
&[
|
||||
Instruction::Call(0), // call the imported function
|
||||
],
|
||||
)),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Mechanisms to generate a function body that can be used inside a `ModuleDefinition`.
|
||||
pub mod body {
|
||||
use super::*;
|
||||
|
||||
pub fn repeated(repetitions: u32, instructions: &[Instruction]) -> FuncBody {
|
||||
repeated_with_locals(&[], repetitions, instructions)
|
||||
}
|
||||
|
||||
pub fn repeated_with_locals(
|
||||
locals: &[Local],
|
||||
repetitions: u32,
|
||||
instructions: &[Instruction],
|
||||
) -> FuncBody {
|
||||
let instructions = Instructions::new(
|
||||
instructions
|
||||
.iter()
|
||||
.cycle()
|
||||
.take(instructions.len() * usize::try_from(repetitions).unwrap())
|
||||
.cloned()
|
||||
.chain(core::iter::once(Instruction::End))
|
||||
.collect(),
|
||||
);
|
||||
FuncBody::new(locals.to_vec(), instructions)
|
||||
}
|
||||
|
||||
pub fn repeated_with_locals_using<const N: usize>(
|
||||
locals: &[Local],
|
||||
repetitions: u32,
|
||||
mut f: impl FnMut() -> [Instruction; N],
|
||||
) -> FuncBody {
|
||||
let mut instructions = Vec::new();
|
||||
for _ in 0..repetitions {
|
||||
instructions.extend(f());
|
||||
}
|
||||
instructions.push(Instruction::End);
|
||||
FuncBody::new(locals.to_vec(), Instructions::new(instructions))
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum amount of pages any contract is allowed to have according to the current `Schedule`.
|
||||
pub fn max_pages<T: Config>() -> u32 {
|
||||
T::Schedule::get().limits.memory_pages
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// ! For instruction benchmarking we do not instantiate a full contract but merely the
|
||||
/// ! sandbox to execute the Wasm code. This is because we do not need the full
|
||||
/// ! environment that provides the seal interface as imported functions.
|
||||
use super::{code::WasmModule, Config};
|
||||
use crate::wasm::{
|
||||
AllowDeprecatedInterface, AllowUnstableInterface, Determinism, Environment, LoadedModule,
|
||||
LoadingMode, WasmBlob,
|
||||
};
|
||||
use pezsp_core::Get;
|
||||
use wasmi::{errors::LinkerError, CompilationMode, Func, Linker, StackLimits, Store};
|
||||
|
||||
/// Minimal execution environment without any imported functions.
|
||||
pub struct Sandbox {
|
||||
entry_point: Func,
|
||||
store: Store<()>,
|
||||
}
|
||||
|
||||
impl Sandbox {
|
||||
/// Invoke the `call` function of a contract code and panic on any execution error.
|
||||
pub fn invoke(&mut self) {
|
||||
self.entry_point.call(&mut self.store, &[], &mut []).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> From<&WasmModule<T>> for Sandbox {
|
||||
/// Creates an instance from the supplied module.
|
||||
/// Sets the execution engine fuel level to `u64::MAX`.
|
||||
fn from(module: &WasmModule<T>) -> Self {
|
||||
let contract = LoadedModule::new::<T>(
|
||||
&module.code,
|
||||
Determinism::Relaxed,
|
||||
Some(StackLimits::default()),
|
||||
LoadingMode::Checked,
|
||||
CompilationMode::Eager,
|
||||
)
|
||||
.expect("Failed to load Wasm module");
|
||||
|
||||
let (mut store, _memory, instance) = WasmBlob::<T>::instantiate::<EmptyEnv, _>(
|
||||
contract,
|
||||
(),
|
||||
&<T>::Schedule::get(),
|
||||
// We are testing with an empty environment anyways
|
||||
AllowDeprecatedInterface::No,
|
||||
)
|
||||
.expect("Failed to create benchmarking Sandbox instance");
|
||||
|
||||
// Set fuel for wasmi execution.
|
||||
store
|
||||
.set_fuel(u64::MAX)
|
||||
.expect("We've set up engine to fuel consuming mode; qed");
|
||||
|
||||
let entry_point = instance
|
||||
.start(&mut store)
|
||||
.unwrap()
|
||||
.get_export(&store, "call")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap();
|
||||
Self { entry_point, store }
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyEnv;
|
||||
|
||||
impl Environment<()> for EmptyEnv {
|
||||
fn define(
|
||||
_: &mut Store<()>,
|
||||
_: &mut Linker<()>,
|
||||
_: AllowUnstableInterface,
|
||||
_: AllowDeprecatedInterface,
|
||||
) -> Result<(), LinkerError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! A mechanism for runtime authors to augment the functionality of contracts.
|
||||
//!
|
||||
//! The runtime is able to call into any contract and retrieve the result using
|
||||
//! [`bare_call`](crate::Pallet::bare_call). This already allows customization of runtime
|
||||
//! behaviour by user generated code (contracts). However, often it is more straightforward
|
||||
//! to allow the reverse behaviour: The contract calls into the runtime. We call the latter
|
||||
//! one a "chain extension" because it allows the chain to extend the set of functions that are
|
||||
//! callable by a contract.
|
||||
//!
|
||||
//! In order to create a chain extension the runtime author implements the [`ChainExtension`]
|
||||
//! trait and declares it in this pallet's [configuration Trait](crate::Config). All types
|
||||
//! required for this endeavour are defined or re-exported in this module. There is an
|
||||
//! implementation on `()` which can be used to signal that no chain extension is available.
|
||||
//!
|
||||
//! # Using multiple chain extensions
|
||||
//!
|
||||
//! Often there is a need for having multiple chain extensions. This is often the case when
|
||||
//! some generally useful off-the-shelf extensions should be included. To have multiple chain
|
||||
//! extensions they can be put into a tuple which is then passed to [`Config::ChainExtension`] like
|
||||
//! this `type Extensions = (ExtensionA, ExtensionB)`.
|
||||
//!
|
||||
//! However, only extensions implementing [`RegisteredChainExtension`] can be put into a tuple.
|
||||
//! This is because the [`RegisteredChainExtension::ID`] is used to decide which of those extensions
|
||||
//! should be used when the contract calls a chain extensions. Extensions which are generally
|
||||
//! useful should claim their `ID` with [the registry](https://github.com/paritytech/chainextension-registry)
|
||||
//! so that no collisions with other vendors will occur.
|
||||
//!
|
||||
//! **Chain specific extensions must use the reserved `ID = 0` so that they can't be registered with
|
||||
//! the registry.**
|
||||
//!
|
||||
//! # Security
|
||||
//!
|
||||
//! The chain author alone is responsible for the security of the chain extension.
|
||||
//! This includes avoiding the exposure of exploitable functions and charging the
|
||||
//! appropriate amount of weight. In order to do so benchmarks must be written and the
|
||||
//! [`charge_weight`](Environment::charge_weight) function must be called **before**
|
||||
//! carrying out any action that causes the consumption of the chargeable weight.
|
||||
//! It cannot be overstated how delicate of a process the creation of a chain extension
|
||||
//! is. Check whether using [`bare_call`](crate::Pallet::bare_call) suffices for the
|
||||
//! use case at hand.
|
||||
//!
|
||||
//! # Benchmarking
|
||||
//!
|
||||
//! The builtin contract callable functions that pezpallet-contracts provides all have
|
||||
//! benchmarks that determine the correct weight that an invocation of these functions
|
||||
//! induces. In order to be able to charge the correct weight for the functions defined
|
||||
//! by a chain extension benchmarks must be written, too. In the near future this crate
|
||||
//! will provide the means for easier creation of those specialized benchmarks.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! The ink-examples repository maintains an
|
||||
//! [end-to-end example](https://github.com/use-ink/ink-examples/tree/v5.x.x/rand-extension)
|
||||
//! on how to use a chain extension in order to provide new features to ink! contracts.
|
||||
|
||||
use crate::{
|
||||
wasm::{Runtime, RuntimeCosts},
|
||||
Error,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, MaxEncodedLen};
|
||||
use core::marker::PhantomData;
|
||||
use pezframe_support::weights::Weight;
|
||||
use pezsp_runtime::DispatchError;
|
||||
|
||||
pub use crate::{exec::Ext, gas::ChargedAmount, storage::meter::Diff, Config};
|
||||
pub use pezframe_system::Config as SysConfig;
|
||||
pub use pezpallet_contracts_uapi::ReturnFlags;
|
||||
|
||||
/// Result that returns a [`DispatchError`] on error.
|
||||
pub type Result<T> = core::result::Result<T, DispatchError>;
|
||||
|
||||
/// A trait used to extend the set of contract callable functions.
|
||||
///
|
||||
/// In order to create a custom chain extension this trait must be implemented and supplied
|
||||
/// to the pallet contracts configuration trait as the associated type of the same name.
|
||||
/// Consult the [module documentation](self) for a general explanation of chain extensions.
|
||||
///
|
||||
/// # Lifetime
|
||||
///
|
||||
/// The extension will be [`Default`] initialized at the beginning of each call
|
||||
/// (**not** per call stack) and dropped afterwards. Hence any value held inside the extension
|
||||
/// can be used as a per-call scratch buffer.
|
||||
pub trait ChainExtension<C: Config> {
|
||||
/// Call the chain extension logic.
|
||||
///
|
||||
/// This is the only function that needs to be implemented in order to write a
|
||||
/// chain extensions. It is called whenever a contract calls the `seal_call_chain_extension`
|
||||
/// imported wasm function.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `env`: Access to the remaining arguments and the execution environment.
|
||||
///
|
||||
/// # Return
|
||||
///
|
||||
/// In case of `Err` the contract execution is immediately suspended and the passed error
|
||||
/// is returned to the caller. Otherwise the value of [`RetVal`] determines the exit
|
||||
/// behaviour.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// The [`Self::call`] can be invoked within a read-only context, where any state-changing calls
|
||||
/// are disallowed. This information can be obtained using `env.ext().is_read_only()`. It is
|
||||
/// crucial for the implementer to handle this scenario appropriately.
|
||||
fn call<E: Ext<T = C>>(&mut self, env: Environment<E, InitState>) -> Result<RetVal>;
|
||||
|
||||
/// Determines whether chain extensions are enabled for this chain.
|
||||
///
|
||||
/// The default implementation returns `true`. Therefore it is not necessary to overwrite
|
||||
/// this function when implementing a chain extension. In case of `false` the deployment of
|
||||
/// a contract that references `seal_call_chain_extension` will be denied and calling this
|
||||
/// function will return [`NoChainExtension`](Error::NoChainExtension) without first calling
|
||||
/// into [`call`](Self::call).
|
||||
fn enabled() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`ChainExtension`] that can be composed with other extensions using a tuple.
|
||||
///
|
||||
/// An extension that implements this trait can be put in a tuple in order to have multiple
|
||||
/// extensions available. The tuple implementation routes requests based on the first two
|
||||
/// most significant bytes of the `id` passed to `call`.
|
||||
///
|
||||
/// If this extensions is to be used by multiple runtimes consider
|
||||
/// [registering it](https://github.com/paritytech/chainextension-registry) to ensure that there
|
||||
/// are no collisions with other vendors.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Currently, we support tuples of up to ten registered chain extensions. If more chain extensions
|
||||
/// are needed consider opening an issue.
|
||||
pub trait RegisteredChainExtension<C: Config>: ChainExtension<C> {
|
||||
/// The extensions globally unique identifier.
|
||||
const ID: u16;
|
||||
}
|
||||
|
||||
#[impl_trait_for_tuples::impl_for_tuples(10)]
|
||||
#[tuple_types_custom_trait_bound(RegisteredChainExtension<C>)]
|
||||
impl<C: Config> ChainExtension<C> for Tuple {
|
||||
fn call<E: Ext<T = C>>(&mut self, mut env: Environment<E, InitState>) -> Result<RetVal> {
|
||||
for_tuples!(
|
||||
#(
|
||||
if (Tuple::ID == env.ext_id()) && Tuple::enabled() {
|
||||
return Tuple.call(env);
|
||||
}
|
||||
)*
|
||||
);
|
||||
Err(Error::<E::T>::NoChainExtension.into())
|
||||
}
|
||||
|
||||
fn enabled() -> bool {
|
||||
for_tuples!(
|
||||
#(
|
||||
if Tuple::enabled() {
|
||||
return true;
|
||||
}
|
||||
)*
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the exit behaviour and return value of a chain extension.
|
||||
pub enum RetVal {
|
||||
/// The chain extensions returns the supplied value to its calling contract.
|
||||
Converging(u32),
|
||||
/// The control does **not** return to the calling contract.
|
||||
///
|
||||
/// Use this to stop the execution of the contract when the chain extension returns.
|
||||
/// The semantic is the same as for calling `seal_return`: The control returns to
|
||||
/// the caller of the currently executing contract yielding the supplied buffer and
|
||||
/// flags.
|
||||
Diverging { flags: ReturnFlags, data: Vec<u8> },
|
||||
}
|
||||
|
||||
/// Grants the chain extension access to its parameters and execution environment.
|
||||
///
|
||||
/// It uses [typestate programming](https://docs.rust-embedded.org/book/static-guarantees/typestate-programming.html)
|
||||
/// to enforce the correct usage of the parameters passed to the chain extension.
|
||||
pub struct Environment<'a, 'b, E: Ext, S: State> {
|
||||
/// The actual data of this type.
|
||||
inner: Inner<'a, 'b, E>,
|
||||
/// `S` is only used in the type system but never as value.
|
||||
phantom: PhantomData<S>,
|
||||
}
|
||||
|
||||
/// Functions that are available in every state of this type.
|
||||
impl<'a, 'b, E: Ext, S: State> Environment<'a, 'b, E, S> {
|
||||
/// The function id within the `id` passed by a contract.
|
||||
///
|
||||
/// It returns the two least significant bytes of the `id` passed by a contract as the other
|
||||
/// two bytes represent the chain extension itself (the code which is calling this function).
|
||||
pub fn func_id(&self) -> u16 {
|
||||
(self.inner.id & 0x0000FFFF) as u16
|
||||
}
|
||||
|
||||
/// The chain extension id within the `id` passed by a contract.
|
||||
///
|
||||
/// It returns the two most significant bytes of the `id` passed by a contract which represent
|
||||
/// the chain extension itself (the code which is calling this function).
|
||||
pub fn ext_id(&self) -> u16 {
|
||||
(self.inner.id >> 16) as u16
|
||||
}
|
||||
|
||||
/// Charge the passed `amount` of weight from the overall limit.
|
||||
///
|
||||
/// It returns `Ok` when there the remaining weight budget is larger than the passed
|
||||
/// `weight`. It returns `Err` otherwise. In this case the chain extension should
|
||||
/// abort the execution and pass through the error.
|
||||
///
|
||||
/// The returned value can be used to with [`Self::adjust_weight`]. Other than that
|
||||
/// it has no purpose.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Weight is synonymous with gas in bizinikiwi.
|
||||
pub fn charge_weight(&mut self, amount: Weight) -> Result<ChargedAmount> {
|
||||
self.inner.runtime.charge_gas(RuntimeCosts::ChainExtension(amount))
|
||||
}
|
||||
|
||||
/// Adjust a previously charged amount down to its actual amount.
|
||||
///
|
||||
/// This is when a maximum a priori amount was charged and then should be partially
|
||||
/// refunded to match the actual amount.
|
||||
pub fn adjust_weight(&mut self, charged: ChargedAmount, actual_weight: Weight) {
|
||||
self.inner
|
||||
.runtime
|
||||
.adjust_gas(charged, RuntimeCosts::ChainExtension(actual_weight))
|
||||
}
|
||||
|
||||
/// Grants access to the execution environment of the current contract call.
|
||||
///
|
||||
/// Consult the functions on the returned type before re-implementing those functions.
|
||||
pub fn ext(&mut self) -> &mut E {
|
||||
self.inner.runtime.ext()
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions that are only available in the initial state of this type.
|
||||
///
|
||||
/// Those are the functions that determine how the arguments to the chain extensions
|
||||
/// should be consumed.
|
||||
impl<'a, 'b, E: Ext> Environment<'a, 'b, E, InitState> {
|
||||
/// Creates a new environment for consumption by a chain extension.
|
||||
///
|
||||
/// It is only available to this crate because only the wasm runtime module needs to
|
||||
/// ever create this type. Chain extensions merely consume it.
|
||||
pub(crate) fn new(
|
||||
runtime: &'a mut Runtime<'b, E>,
|
||||
memory: &'a mut [u8],
|
||||
id: u32,
|
||||
input_ptr: u32,
|
||||
input_len: u32,
|
||||
output_ptr: u32,
|
||||
output_len_ptr: u32,
|
||||
) -> Self {
|
||||
Environment {
|
||||
inner: Inner { runtime, memory, id, input_ptr, input_len, output_ptr, output_len_ptr },
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use all arguments as integer values.
|
||||
pub fn only_in(self) -> Environment<'a, 'b, E, OnlyInState> {
|
||||
Environment { inner: self.inner, phantom: PhantomData }
|
||||
}
|
||||
|
||||
/// Use input arguments as integer and output arguments as pointer to a buffer.
|
||||
pub fn prim_in_buf_out(self) -> Environment<'a, 'b, E, PrimInBufOutState> {
|
||||
Environment { inner: self.inner, phantom: PhantomData }
|
||||
}
|
||||
|
||||
/// Use input and output arguments as pointers to a buffer.
|
||||
pub fn buf_in_buf_out(self) -> Environment<'a, 'b, E, BufInBufOutState> {
|
||||
Environment { inner: self.inner, phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions to use the input arguments as integers.
|
||||
impl<'a, 'b, E: Ext, S: PrimIn> Environment<'a, 'b, E, S> {
|
||||
/// The `input_ptr` argument.
|
||||
pub fn val0(&self) -> u32 {
|
||||
self.inner.input_ptr
|
||||
}
|
||||
|
||||
/// The `input_len` argument.
|
||||
pub fn val1(&self) -> u32 {
|
||||
self.inner.input_len
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions to use the output arguments as integers.
|
||||
impl<'a, 'b, E: Ext, S: PrimOut> Environment<'a, 'b, E, S> {
|
||||
/// The `output_ptr` argument.
|
||||
pub fn val2(&self) -> u32 {
|
||||
self.inner.output_ptr
|
||||
}
|
||||
|
||||
/// The `output_len_ptr` argument.
|
||||
pub fn val3(&self) -> u32 {
|
||||
self.inner.output_len_ptr
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions to use the input arguments as pointer to a buffer.
|
||||
impl<'a, 'b, E: Ext, S: BufIn> Environment<'a, 'b, E, S> {
|
||||
/// Reads `min(max_len, in_len)` from contract memory.
|
||||
///
|
||||
/// This does **not** charge any weight. The caller must make sure that the an
|
||||
/// appropriate amount of weight is charged **before** reading from contract memory.
|
||||
/// The reason for that is that usually the costs for reading data and processing
|
||||
/// said data cannot be separated in a benchmark. Therefore a chain extension would
|
||||
/// charge the overall costs either using `max_len` (worst case approximation) or using
|
||||
/// [`in_len()`](Self::in_len).
|
||||
pub fn read(&self, max_len: u32) -> Result<Vec<u8>> {
|
||||
self.inner.runtime.read_sandbox_memory(
|
||||
self.inner.memory,
|
||||
self.inner.input_ptr,
|
||||
self.inner.input_len.min(max_len),
|
||||
)
|
||||
}
|
||||
|
||||
/// Reads `min(buffer.len(), in_len) from contract memory.
|
||||
///
|
||||
/// This takes a mutable pointer to a buffer fills it with data and shrinks it to
|
||||
/// the size of the actual data. Apart from supporting pre-allocated buffers it is
|
||||
/// equivalent to to [`read()`](Self::read).
|
||||
pub fn read_into(&self, buffer: &mut &mut [u8]) -> Result<()> {
|
||||
let len = buffer.len();
|
||||
let sliced = {
|
||||
let buffer = core::mem::take(buffer);
|
||||
&mut buffer[..len.min(self.inner.input_len as usize)]
|
||||
};
|
||||
self.inner.runtime.read_sandbox_memory_into_buf(
|
||||
self.inner.memory,
|
||||
self.inner.input_ptr,
|
||||
sliced,
|
||||
)?;
|
||||
*buffer = sliced;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads and decodes a type with a size fixed at compile time from contract memory.
|
||||
///
|
||||
/// This function is secure and recommended for all input types of fixed size
|
||||
/// as long as the cost of reading the memory is included in the overall already charged
|
||||
/// weight of the chain extension. This should usually be the case when fixed input types
|
||||
/// are used.
|
||||
pub fn read_as<T: Decode + MaxEncodedLen>(&mut self) -> Result<T> {
|
||||
self.inner
|
||||
.runtime
|
||||
.read_sandbox_memory_as(self.inner.memory, self.inner.input_ptr)
|
||||
}
|
||||
|
||||
/// Reads and decodes a type with a dynamic size from contract memory.
|
||||
///
|
||||
/// Make sure to include `len` in your weight calculations.
|
||||
pub fn read_as_unbounded<T: Decode>(&mut self, len: u32) -> Result<T> {
|
||||
self.inner.runtime.read_sandbox_memory_as_unbounded(
|
||||
self.inner.memory,
|
||||
self.inner.input_ptr,
|
||||
len,
|
||||
)
|
||||
}
|
||||
|
||||
/// The length of the input as passed in as `input_len`.
|
||||
///
|
||||
/// A chain extension would use this value to calculate the dynamic part of its
|
||||
/// weight. For example a chain extension that calculates the hash of some passed in
|
||||
/// bytes would use `in_len` to charge the costs of hashing that amount of bytes.
|
||||
/// This also subsumes the act of copying those bytes as a benchmarks measures both.
|
||||
pub fn in_len(&self) -> u32 {
|
||||
self.inner.input_len
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions to use the output arguments as pointer to a buffer.
|
||||
impl<'a, 'b, E: Ext, S: BufOut> Environment<'a, 'b, E, S> {
|
||||
/// Write the supplied buffer to contract memory.
|
||||
///
|
||||
/// If the contract supplied buffer is smaller than the passed `buffer` an `Err` is returned.
|
||||
/// If `allow_skip` is set to true the contract is allowed to skip the copying of the buffer
|
||||
/// by supplying the guard value of `pezpallet-contracts::SENTINEL` as `out_ptr`. The
|
||||
/// `weight_per_byte` is only charged when the write actually happens and is not skipped or
|
||||
/// failed due to a too small output buffer.
|
||||
pub fn write(
|
||||
&mut self,
|
||||
buffer: &[u8],
|
||||
allow_skip: bool,
|
||||
weight_per_byte: Option<Weight>,
|
||||
) -> Result<()> {
|
||||
self.inner.runtime.write_sandbox_output(
|
||||
self.inner.memory,
|
||||
self.inner.output_ptr,
|
||||
self.inner.output_len_ptr,
|
||||
buffer,
|
||||
allow_skip,
|
||||
|len| {
|
||||
weight_per_byte.map(|w| RuntimeCosts::ChainExtension(w.saturating_mul(len.into())))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The actual data of an `Environment`.
|
||||
///
|
||||
/// All data is put into this struct to easily pass it around as part of the typestate
|
||||
/// pattern. Also it creates the opportunity to box this struct in the future in case it
|
||||
/// gets too large.
|
||||
struct Inner<'a, 'b, E: Ext> {
|
||||
/// The runtime contains all necessary functions to interact with the running contract.
|
||||
runtime: &'a mut Runtime<'b, E>,
|
||||
/// Reference to the contracts memory.
|
||||
memory: &'a mut [u8],
|
||||
/// Verbatim argument passed to `seal_call_chain_extension`.
|
||||
id: u32,
|
||||
/// Verbatim argument passed to `seal_call_chain_extension`.
|
||||
input_ptr: u32,
|
||||
/// Verbatim argument passed to `seal_call_chain_extension`.
|
||||
input_len: u32,
|
||||
/// Verbatim argument passed to `seal_call_chain_extension`.
|
||||
output_ptr: u32,
|
||||
/// Verbatim argument passed to `seal_call_chain_extension`.
|
||||
output_len_ptr: u32,
|
||||
}
|
||||
|
||||
/// Any state of an [`Environment`] implements this trait.
|
||||
/// See [typestate programming](https://docs.rust-embedded.org/book/static-guarantees/typestate-programming.html).
|
||||
pub trait State: sealed::Sealed {}
|
||||
|
||||
/// A state that uses primitive inputs.
|
||||
pub trait PrimIn: State {}
|
||||
|
||||
/// A state that uses primitive outputs.
|
||||
pub trait PrimOut: State {}
|
||||
|
||||
/// A state that uses a buffer as input.
|
||||
pub trait BufIn: State {}
|
||||
|
||||
/// A state that uses a buffer as output.
|
||||
pub trait BufOut: State {}
|
||||
|
||||
/// The initial state of an [`Environment`].
|
||||
pub enum InitState {}
|
||||
|
||||
/// A state that uses all arguments as primitive inputs.
|
||||
pub enum OnlyInState {}
|
||||
|
||||
/// A state that uses two arguments as primitive inputs and the other two as buffer output.
|
||||
pub enum PrimInBufOutState {}
|
||||
|
||||
/// Uses a buffer for input and a buffer for output.
|
||||
pub enum BufInBufOutState {}
|
||||
|
||||
mod sealed {
|
||||
use super::*;
|
||||
|
||||
/// Trait to prevent users from implementing `State` for anything else.
|
||||
pub trait Sealed {}
|
||||
|
||||
impl Sealed for InitState {}
|
||||
impl Sealed for OnlyInState {}
|
||||
impl Sealed for PrimInBufOutState {}
|
||||
impl Sealed for BufInBufOutState {}
|
||||
|
||||
impl State for InitState {}
|
||||
impl State for OnlyInState {}
|
||||
impl State for PrimInBufOutState {}
|
||||
impl State for BufInBufOutState {}
|
||||
|
||||
impl PrimIn for OnlyInState {}
|
||||
impl PrimOut for OnlyInState {}
|
||||
impl PrimIn for PrimInBufOutState {}
|
||||
impl BufOut for PrimInBufOutState {}
|
||||
impl BufIn for BufInBufOutState {}
|
||||
impl BufOut for BufInBufOutState {}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub use crate::{
|
||||
exec::{ExecResult, ExportedFunction},
|
||||
primitives::ExecReturnValue,
|
||||
};
|
||||
use crate::{Config, LOG_TARGET};
|
||||
|
||||
/// Umbrella trait for all interfaces that serves for debugging.
|
||||
pub trait Debugger<T: Config>: Tracing<T> + CallInterceptor<T> {}
|
||||
|
||||
impl<T: Config, V> Debugger<T> for V where V: Tracing<T> + CallInterceptor<T> {}
|
||||
|
||||
/// Defines methods to capture contract calls, enabling external observers to
|
||||
/// measure, trace, and react to contract interactions.
|
||||
pub trait Tracing<T: Config> {
|
||||
/// The type of [`CallSpan`] that is created by this trait.
|
||||
type CallSpan: CallSpan;
|
||||
|
||||
/// Creates a new call span to encompass the upcoming contract execution.
|
||||
///
|
||||
/// This method should be invoked just before the execution of a contract and
|
||||
/// marks the beginning of a traceable span of execution.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `contract_address` - The address of the contract that is about to be executed.
|
||||
/// * `entry_point` - Describes whether the call is the constructor or a regular call.
|
||||
/// * `input_data` - The raw input data of the call.
|
||||
fn new_call_span(
|
||||
contract_address: &T::AccountId,
|
||||
entry_point: ExportedFunction,
|
||||
input_data: &[u8],
|
||||
) -> Self::CallSpan;
|
||||
}
|
||||
|
||||
/// Defines a span of execution for a contract call.
|
||||
pub trait CallSpan {
|
||||
/// Called just after the execution of a contract.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `output` - The raw output of the call.
|
||||
fn after_call(self, output: &ExecReturnValue);
|
||||
}
|
||||
|
||||
impl<T: Config> Tracing<T> for () {
|
||||
type CallSpan = ();
|
||||
|
||||
fn new_call_span(
|
||||
contract_address: &T::AccountId,
|
||||
entry_point: ExportedFunction,
|
||||
input_data: &[u8],
|
||||
) {
|
||||
log::trace!(target: LOG_TARGET, "call {entry_point:?} account: {contract_address:?}, input_data: {input_data:?}")
|
||||
}
|
||||
}
|
||||
|
||||
impl CallSpan for () {
|
||||
fn after_call(self, output: &ExecReturnValue) {
|
||||
log::trace!(target: LOG_TARGET, "call result {output:?}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an interface for intercepting contract calls.
|
||||
pub trait CallInterceptor<T: Config> {
|
||||
/// Allows to intercept contract calls and decide whether they should be executed or not.
|
||||
/// If the call is intercepted, the mocked result of the call is returned.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `contract_address` - The address of the contract that is about to be executed.
|
||||
/// * `entry_point` - Describes whether the call is the constructor or a regular call.
|
||||
/// * `input_data` - The raw input data of the call.
|
||||
///
|
||||
/// # Expected behavior
|
||||
///
|
||||
/// This method should return:
|
||||
/// * `Some(ExecResult)` - if the call should be intercepted and the mocked result of the call
|
||||
/// is returned.
|
||||
/// * `None` - otherwise, i.e. the call should be executed normally.
|
||||
fn intercept_call(
|
||||
contract_address: &T::AccountId,
|
||||
entry_point: &ExportedFunction,
|
||||
input_data: &[u8],
|
||||
) -> Option<ExecResult>;
|
||||
}
|
||||
|
||||
impl<T: Config> CallInterceptor<T> for () {
|
||||
fn intercept_call(
|
||||
_contract_address: &T::AccountId,
|
||||
_entry_point: &ExportedFunction,
|
||||
_input_data: &[u8],
|
||||
) -> Option<ExecResult> {
|
||||
None
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{exec::ExecError, Config, Error};
|
||||
use core::marker::PhantomData;
|
||||
use pezframe_support::{
|
||||
dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, PostDispatchInfo},
|
||||
weights::Weight,
|
||||
DefaultNoBound,
|
||||
};
|
||||
use pezsp_core::Get;
|
||||
use pezsp_runtime::{traits::Zero, DispatchError};
|
||||
|
||||
#[cfg(test)]
|
||||
use std::{any::Any, fmt::Debug};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ChargedAmount(Weight);
|
||||
|
||||
impl ChargedAmount {
|
||||
pub fn amount(&self) -> Weight {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Meter for syncing the gas between the executor and the gas meter.
|
||||
#[derive(DefaultNoBound)]
|
||||
struct EngineMeter<T: Config> {
|
||||
fuel: u64,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> EngineMeter<T> {
|
||||
/// Create a meter with the given fuel limit.
|
||||
fn new(limit: Weight) -> Self {
|
||||
Self {
|
||||
fuel: limit.ref_time().saturating_div(T::Schedule::get().ref_time_by_fuel()),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the fuel left to the given value.
|
||||
/// Returns the amount of Weight consumed since the last update.
|
||||
fn set_fuel(&mut self, fuel: u64) -> Weight {
|
||||
let consumed = self
|
||||
.fuel
|
||||
.saturating_sub(fuel)
|
||||
.saturating_mul(T::Schedule::get().ref_time_by_fuel());
|
||||
self.fuel = fuel;
|
||||
Weight::from_parts(consumed, 0)
|
||||
}
|
||||
|
||||
/// Charge the given amount of gas.
|
||||
/// Returns the amount of fuel left.
|
||||
fn charge_ref_time(&mut self, ref_time: u64) -> Result<Syncable, DispatchError> {
|
||||
let amount = ref_time
|
||||
.checked_div(T::Schedule::get().ref_time_by_fuel())
|
||||
.ok_or(Error::<T>::InvalidSchedule)?;
|
||||
|
||||
self.fuel.checked_sub(amount).ok_or_else(|| Error::<T>::OutOfGas)?;
|
||||
Ok(Syncable(self.fuel))
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to capture the gas left before entering a host function.
|
||||
///
|
||||
/// Has to be consumed in order to sync back the gas after leaving the host function.
|
||||
#[must_use]
|
||||
pub struct RefTimeLeft(u64);
|
||||
|
||||
/// Resource that needs to be synced to the executor.
|
||||
///
|
||||
/// Wrapped to make sure that the resource will be synced back the the executor.
|
||||
#[must_use]
|
||||
pub struct Syncable(u64);
|
||||
|
||||
impl From<Syncable> for u64 {
|
||||
fn from(from: Syncable) -> u64 {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub trait TestAuxiliaries {}
|
||||
#[cfg(not(test))]
|
||||
impl<T> TestAuxiliaries for T {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub trait TestAuxiliaries: Any + Debug + PartialEq + Eq {}
|
||||
#[cfg(test)]
|
||||
impl<T: Any + Debug + PartialEq + Eq> TestAuxiliaries for T {}
|
||||
|
||||
/// This trait represents a token that can be used for charging `GasMeter`.
|
||||
/// There is no other way of charging it.
|
||||
///
|
||||
/// Implementing type is expected to be super lightweight hence `Copy` (`Clone` is added
|
||||
/// for consistency). If inlined there should be no observable difference compared
|
||||
/// to a hand-written code.
|
||||
pub trait Token<T: Config>: Copy + Clone + TestAuxiliaries {
|
||||
/// Return the amount of gas that should be taken by this token.
|
||||
///
|
||||
/// This function should be really lightweight and must not fail. It is not
|
||||
/// expected that implementors will query the storage or do any kinds of heavy operations.
|
||||
///
|
||||
/// That said, implementors of this function still can run into overflows
|
||||
/// while calculating the amount. In this case it is ok to use saturating operations
|
||||
/// since on overflow they will return `max_value` which should consume all gas.
|
||||
fn weight(&self) -> Weight;
|
||||
|
||||
/// Returns true if this token is expected to influence the lowest gas limit.
|
||||
fn influence_lowest_gas_limit(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a type-erased trait object of what used to be a `Token`.
|
||||
#[cfg(test)]
|
||||
pub struct ErasedToken {
|
||||
pub description: String,
|
||||
pub token: Box<dyn Any>,
|
||||
}
|
||||
|
||||
#[derive(DefaultNoBound)]
|
||||
pub struct GasMeter<T: Config> {
|
||||
gas_limit: Weight,
|
||||
/// Amount of gas left from initial gas limit. Can reach zero.
|
||||
gas_left: Weight,
|
||||
/// Due to `adjust_gas` and `nested` the `gas_left` can temporarily dip below its final value.
|
||||
gas_left_lowest: Weight,
|
||||
/// The amount of resources that was consumed by the execution engine.
|
||||
/// We have to track it separately in order to avoid the loss of precision that happens when
|
||||
/// converting from ref_time to the execution engine unit.
|
||||
engine_meter: EngineMeter<T>,
|
||||
_phantom: PhantomData<T>,
|
||||
#[cfg(test)]
|
||||
tokens: Vec<ErasedToken>,
|
||||
}
|
||||
|
||||
impl<T: Config> GasMeter<T> {
|
||||
pub fn new(gas_limit: Weight) -> Self {
|
||||
GasMeter {
|
||||
gas_limit,
|
||||
gas_left: gas_limit,
|
||||
gas_left_lowest: gas_limit,
|
||||
engine_meter: EngineMeter::new(gas_limit),
|
||||
_phantom: PhantomData,
|
||||
#[cfg(test)]
|
||||
tokens: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new gas meter by removing gas from the current meter.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Passing `0` as amount is interpreted as "all remaining gas".
|
||||
pub fn nested(&mut self, amount: Weight) -> Self {
|
||||
let amount = Weight::from_parts(
|
||||
if amount.ref_time().is_zero() {
|
||||
self.gas_left().ref_time()
|
||||
} else {
|
||||
amount.ref_time()
|
||||
},
|
||||
if amount.proof_size().is_zero() {
|
||||
self.gas_left().proof_size()
|
||||
} else {
|
||||
amount.proof_size()
|
||||
},
|
||||
)
|
||||
.min(self.gas_left);
|
||||
self.gas_left -= amount;
|
||||
GasMeter::new(amount)
|
||||
}
|
||||
|
||||
/// Absorb the remaining gas of a nested meter after we are done using it.
|
||||
pub fn absorb_nested(&mut self, nested: Self) {
|
||||
self.gas_left_lowest = (self.gas_left + nested.gas_limit)
|
||||
.saturating_sub(nested.gas_required())
|
||||
.min(self.gas_left_lowest);
|
||||
self.gas_left += nested.gas_left;
|
||||
}
|
||||
|
||||
/// Account for used gas.
|
||||
///
|
||||
/// Amount is calculated by the given `token`.
|
||||
///
|
||||
/// Returns `OutOfGas` if there is not enough gas or addition of the specified
|
||||
/// amount of gas has lead to overflow.
|
||||
///
|
||||
/// NOTE that amount isn't consumed if there is not enough gas. This is considered
|
||||
/// safe because we always charge gas before performing any resource-spending action.
|
||||
#[inline]
|
||||
pub fn charge<Tok: Token<T>>(&mut self, token: Tok) -> Result<ChargedAmount, DispatchError> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
// Unconditionally add the token to the storage.
|
||||
let erased_tok =
|
||||
ErasedToken { description: format!("{:?}", token), token: Box::new(token) };
|
||||
self.tokens.push(erased_tok);
|
||||
}
|
||||
let amount = token.weight();
|
||||
// It is OK to not charge anything on failure because we always charge _before_ we perform
|
||||
// any action
|
||||
self.gas_left = self.gas_left.checked_sub(&amount).ok_or_else(|| Error::<T>::OutOfGas)?;
|
||||
Ok(ChargedAmount(amount))
|
||||
}
|
||||
|
||||
/// Adjust a previously charged amount down to its actual amount.
|
||||
///
|
||||
/// This is when a maximum a priori amount was charged and then should be partially
|
||||
/// refunded to match the actual amount.
|
||||
pub fn adjust_gas<Tok: Token<T>>(&mut self, charged_amount: ChargedAmount, token: Tok) {
|
||||
if token.influence_lowest_gas_limit() {
|
||||
self.gas_left_lowest = self.gas_left_lowest();
|
||||
}
|
||||
let adjustment = charged_amount.0.saturating_sub(token.weight());
|
||||
self.gas_left = self.gas_left.saturating_add(adjustment).min(self.gas_limit);
|
||||
}
|
||||
|
||||
/// Hand over the gas metering responsibility from the executor to this meter.
|
||||
///
|
||||
/// Needs to be called when entering a host function to update this meter with the
|
||||
/// gas that was tracked by the executor. It tracks the latest seen total value
|
||||
/// in order to compute the delta that needs to be charged.
|
||||
pub fn sync_from_executor(&mut self, engine_fuel: u64) -> Result<RefTimeLeft, DispatchError> {
|
||||
let weight_consumed = self.engine_meter.set_fuel(engine_fuel);
|
||||
self.gas_left
|
||||
.checked_reduce(weight_consumed)
|
||||
.ok_or_else(|| Error::<T>::OutOfGas)?;
|
||||
Ok(RefTimeLeft(self.gas_left.ref_time()))
|
||||
}
|
||||
|
||||
/// Hand over the gas metering responsibility from this meter to the executor.
|
||||
///
|
||||
/// Needs to be called when leaving a host function in order to calculate how much
|
||||
/// gas needs to be charged from the **executor**. It updates the last seen executor
|
||||
/// total value so that it is correct when `sync_from_executor` is called the next time.
|
||||
///
|
||||
/// It is important that this does **not** actually sync with the executor. That has
|
||||
/// to be done by the caller.
|
||||
pub fn sync_to_executor(&mut self, before: RefTimeLeft) -> Result<Syncable, DispatchError> {
|
||||
let ref_time_consumed = before.0.saturating_sub(self.gas_left().ref_time());
|
||||
self.engine_meter.charge_ref_time(ref_time_consumed)
|
||||
}
|
||||
|
||||
/// Returns the amount of gas that is required to run the same call.
|
||||
///
|
||||
/// This can be different from `gas_spent` because due to `adjust_gas` the amount of
|
||||
/// spent gas can temporarily drop and be refunded later.
|
||||
pub fn gas_required(&self) -> Weight {
|
||||
self.gas_limit.saturating_sub(self.gas_left_lowest())
|
||||
}
|
||||
|
||||
/// Returns how much gas was spent
|
||||
pub fn gas_consumed(&self) -> Weight {
|
||||
self.gas_limit.saturating_sub(self.gas_left)
|
||||
}
|
||||
|
||||
/// Returns how much gas left from the initial budget.
|
||||
pub fn gas_left(&self) -> Weight {
|
||||
self.gas_left
|
||||
}
|
||||
|
||||
/// Turn this GasMeter into a DispatchResult that contains the actually used gas.
|
||||
pub fn into_dispatch_result<R, E>(
|
||||
self,
|
||||
result: Result<R, E>,
|
||||
base_weight: Weight,
|
||||
) -> DispatchResultWithPostInfo
|
||||
where
|
||||
E: Into<ExecError>,
|
||||
{
|
||||
let post_info = PostDispatchInfo {
|
||||
actual_weight: Some(self.gas_consumed().saturating_add(base_weight)),
|
||||
pays_fee: Default::default(),
|
||||
};
|
||||
|
||||
result
|
||||
.map(|_| post_info)
|
||||
.map_err(|e| DispatchErrorWithPostInfo { post_info, error: e.into().error })
|
||||
}
|
||||
|
||||
fn gas_left_lowest(&self) -> Weight {
|
||||
self.gas_left_lowest.min(self.gas_left)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn tokens(&self) -> &[ErasedToken] {
|
||||
&self.tokens
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{GasMeter, Token, Weight};
|
||||
use crate::tests::Test;
|
||||
|
||||
/// A simple utility macro that helps to match against a
|
||||
/// list of tokens.
|
||||
macro_rules! match_tokens {
|
||||
($tokens_iter:ident,) => {
|
||||
};
|
||||
($tokens_iter:ident, $x:expr, $($rest:tt)*) => {
|
||||
{
|
||||
let next = ($tokens_iter).next().unwrap();
|
||||
let pattern = $x;
|
||||
|
||||
// Note that we don't specify the type name directly in this macro,
|
||||
// we only have some expression $x of some type. At the same time, we
|
||||
// have an iterator of Box<dyn Any> and to downcast we need to specify
|
||||
// the type which we want downcast to.
|
||||
//
|
||||
// So what we do is we assign `_pattern_typed_next_ref` to a variable which has
|
||||
// the required type.
|
||||
//
|
||||
// Then we make `_pattern_typed_next_ref = token.downcast_ref()`. This makes
|
||||
// rustc infer the type `T` (in `downcast_ref<T: Any>`) to be the same as in $x.
|
||||
|
||||
let mut _pattern_typed_next_ref = &pattern;
|
||||
_pattern_typed_next_ref = match next.token.downcast_ref() {
|
||||
Some(p) => {
|
||||
assert_eq!(p, &pattern);
|
||||
p
|
||||
}
|
||||
None => {
|
||||
panic!("expected type {} got {}", stringify!($x), next.description);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match_tokens!($tokens_iter, $($rest)*);
|
||||
};
|
||||
}
|
||||
|
||||
/// A trivial token that charges the specified number of gas units.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
struct SimpleToken(u64);
|
||||
impl Token<Test> for SimpleToken {
|
||||
fn weight(&self) -> Weight {
|
||||
Weight::from_parts(self.0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let gas_meter = GasMeter::<Test>::new(Weight::from_parts(50000, 0));
|
||||
assert_eq!(gas_meter.gas_left(), Weight::from_parts(50000, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracing() {
|
||||
let mut gas_meter = GasMeter::<Test>::new(Weight::from_parts(50000, 0));
|
||||
assert!(!gas_meter.charge(SimpleToken(1)).is_err());
|
||||
|
||||
let mut tokens = gas_meter.tokens().iter();
|
||||
match_tokens!(tokens, SimpleToken(1),);
|
||||
}
|
||||
|
||||
// This test makes sure that nothing can be executed if there is no gas.
|
||||
#[test]
|
||||
fn refuse_to_execute_anything_if_zero() {
|
||||
let mut gas_meter = GasMeter::<Test>::new(Weight::zero());
|
||||
assert!(gas_meter.charge(SimpleToken(1)).is_err());
|
||||
}
|
||||
|
||||
// Make sure that the gas meter does not charge in case of overcharge
|
||||
#[test]
|
||||
fn overcharge_does_not_charge() {
|
||||
let mut gas_meter = GasMeter::<Test>::new(Weight::from_parts(200, 0));
|
||||
|
||||
// The first charge is should lead to OOG.
|
||||
assert!(gas_meter.charge(SimpleToken(300)).is_err());
|
||||
|
||||
// The gas meter should still contain the full 200.
|
||||
assert!(gas_meter.charge(SimpleToken(200)).is_ok());
|
||||
}
|
||||
|
||||
// Charging the exact amount that the user paid for should be
|
||||
// possible.
|
||||
#[test]
|
||||
fn charge_exact_amount() {
|
||||
let mut gas_meter = GasMeter::<Test>::new(Weight::from_parts(25, 0));
|
||||
assert!(!gas_meter.charge(SimpleToken(25)).is_err());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,658 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Multi-block Migration framework for pezpallet-contracts.
|
||||
//!
|
||||
//! This module allows us to define a migration as a sequence of [`MigrationStep`]s that can be
|
||||
//! executed across multiple blocks.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! A migration step is defined under `src/migration/vX.rs`, where `X` is the version number.
|
||||
//! For example, `vX.rs` defines a migration from version `X - 1` to version `X`.
|
||||
//!
|
||||
//! ## Example:
|
||||
//!
|
||||
//! To configure a migration to `v11` for a runtime using `v10` of pezpallet-contracts on the chain,
|
||||
//! you would set the `Migrations` type as follows:
|
||||
//!
|
||||
//! ```
|
||||
//! use pezpallet_contracts::migration::{v10, v11};
|
||||
//! # pub enum Runtime {};
|
||||
//! # struct Currency;
|
||||
//! type Migrations = (v10::Migration<Runtime, Currency>, v11::Migration<Runtime>);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Notes:
|
||||
//!
|
||||
//! - Migrations should always be tested with `try-runtime` before being deployed.
|
||||
//! - By testing with `try-runtime` against a live network, you ensure that all migration steps work
|
||||
//! and that you have included the required steps.
|
||||
//!
|
||||
//! ## Low Level / Implementation Details
|
||||
//!
|
||||
//! When a migration starts and [`OnRuntimeUpgrade::on_runtime_upgrade`] is called, instead of
|
||||
//! performing the actual migration, we set a custom storage item [`MigrationInProgress`].
|
||||
//! This storage item defines a [`Cursor`] for the current migration.
|
||||
//!
|
||||
//! If the [`MigrationInProgress`] storage item exists, it means a migration is in progress, and its
|
||||
//! value holds a cursor for the current migration step. These migration steps are executed during
|
||||
//! [`Hooks<BlockNumber>::on_idle`] or when the [`Pallet::migrate`] dispatchable is
|
||||
//! called.
|
||||
//!
|
||||
//! While the migration is in progress, all dispatchables except `migrate`, are blocked, and returns
|
||||
//! a `MigrationInProgress` error.
|
||||
|
||||
pub mod v09;
|
||||
pub mod v10;
|
||||
pub mod v11;
|
||||
pub mod v12;
|
||||
pub mod v13;
|
||||
pub mod v14;
|
||||
pub mod v15;
|
||||
pub mod v16;
|
||||
include!(concat!(env!("OUT_DIR"), "/migration_codegen.rs"));
|
||||
|
||||
use crate::{weights::WeightInfo, Config, Error, MigrationInProgress, Pallet, Weight, LOG_TARGET};
|
||||
use codec::{Codec, Decode};
|
||||
use core::marker::PhantomData;
|
||||
use pezframe_support::{
|
||||
pezpallet_prelude::*,
|
||||
traits::{ConstU32, OnRuntimeUpgrade},
|
||||
weights::WeightMeter,
|
||||
};
|
||||
use pezsp_runtime::Saturating;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
const PROOF_ENCODE: &str = "Tuple::max_encoded_len() < Cursor::max_encoded_len()` is verified in `Self::integrity_test()`; qed";
|
||||
const PROOF_DECODE: &str =
|
||||
"We encode to the same type in this trait only. No other code touches this item; qed";
|
||||
|
||||
fn invalid_version(version: StorageVersion) -> ! {
|
||||
panic!("Required migration {version:?} not supported by this runtime. This is a bug.");
|
||||
}
|
||||
|
||||
/// The cursor used to encode the position (usually the last iterated key) of the current migration
|
||||
/// step.
|
||||
pub type Cursor = BoundedVec<u8, ConstU32<1024>>;
|
||||
|
||||
/// IsFinished describes whether a migration is finished or not.
|
||||
pub enum IsFinished {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// A trait that allows to migrate storage from one version to another.
|
||||
///
|
||||
/// The migration is done in steps. The migration is finished when
|
||||
/// `step()` returns `IsFinished::Yes`.
|
||||
pub trait MigrationStep: Codec + MaxEncodedLen + Default {
|
||||
/// Returns the version of the migration.
|
||||
const VERSION: u16;
|
||||
|
||||
/// Returns the maximum weight that can be consumed in a single step.
|
||||
fn max_step_weight() -> Weight;
|
||||
|
||||
/// Process one step of the migration.
|
||||
///
|
||||
/// Returns whether the migration is finished.
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished;
|
||||
|
||||
/// Verify that the migration step fits into `Cursor`, and that `max_step_weight` is not greater
|
||||
/// than `max_block_weight`.
|
||||
fn integrity_test(max_block_weight: Weight) {
|
||||
if Self::max_step_weight().any_gt(max_block_weight) {
|
||||
panic!(
|
||||
"Invalid max_step_weight for Migration {}. Value should be lower than {}",
|
||||
Self::VERSION,
|
||||
max_block_weight
|
||||
);
|
||||
}
|
||||
|
||||
let len = <Self as MaxEncodedLen>::max_encoded_len();
|
||||
let max = Cursor::bound();
|
||||
if len > max {
|
||||
panic!(
|
||||
"Migration {} has size {} which is bigger than the maximum of {}",
|
||||
Self::VERSION,
|
||||
len,
|
||||
max,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute some pre-checks prior to running the first step of this migration.
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade_step() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Execute some post-checks after running the last step of this migration.
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade_step(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A noop migration that can be used when there is no migration to be done for a given version.
|
||||
#[doc(hidden)]
|
||||
#[derive(pezframe_support::DefaultNoBound, Encode, Decode, MaxEncodedLen)]
|
||||
pub struct NoopMigration<const N: u16>;
|
||||
|
||||
impl<const N: u16> MigrationStep for NoopMigration<N> {
|
||||
const VERSION: u16 = N;
|
||||
fn max_step_weight() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn step(&mut self, _meter: &mut WeightMeter) -> IsFinished {
|
||||
log::debug!(target: LOG_TARGET, "Noop migration for version {}", N);
|
||||
IsFinished::Yes
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use crate::migration::MigrationStep;
|
||||
pub trait Sealed {}
|
||||
#[impl_trait_for_tuples::impl_for_tuples(10)]
|
||||
#[tuple_types_custom_trait_bound(MigrationStep)]
|
||||
impl Sealed for Tuple {}
|
||||
}
|
||||
|
||||
/// Defines a sequence of migrations.
|
||||
///
|
||||
/// The sequence must be defined by a tuple of migrations, each of which must implement the
|
||||
/// `MigrationStep` trait. Migrations must be ordered by their versions with no gaps.
|
||||
pub trait MigrateSequence: private::Sealed {
|
||||
/// Returns the range of versions that this migrations sequence can handle.
|
||||
/// Migrations must be ordered by their versions with no gaps.
|
||||
///
|
||||
/// The following code will fail to compile:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # use pezpallet_contracts::{NoopMigration, MigrateSequence};
|
||||
/// let _ = <(NoopMigration<1>, NoopMigration<3>)>::VERSION_RANGE;
|
||||
/// ```
|
||||
/// The following code will compile:
|
||||
/// ```
|
||||
/// # use pezpallet_contracts::{NoopMigration, MigrateSequence};
|
||||
/// let _ = <(NoopMigration<1>, NoopMigration<2>)>::VERSION_RANGE;
|
||||
/// ```
|
||||
const VERSION_RANGE: (u16, u16);
|
||||
|
||||
/// Returns the default cursor for the given version.
|
||||
fn new(version: StorageVersion) -> Cursor;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade_step(_version: StorageVersion) -> Result<Vec<u8>, TryRuntimeError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade_step(_version: StorageVersion, _state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the migration step until the available weight is consumed.
|
||||
fn steps(version: StorageVersion, cursor: &[u8], meter: &mut WeightMeter) -> StepResult;
|
||||
|
||||
/// Verify that the migration step fits into `Cursor`, and that `max_step_weight` is not greater
|
||||
/// than `max_block_weight`.
|
||||
fn integrity_test(max_block_weight: Weight);
|
||||
|
||||
/// Returns whether migrating from `in_storage` to `target` is supported.
|
||||
///
|
||||
/// A migration is supported if `VERSION_RANGE` is (in_storage + 1, target).
|
||||
fn is_upgrade_supported(in_storage: StorageVersion, target: StorageVersion) -> bool {
|
||||
let (low, high) = Self::VERSION_RANGE;
|
||||
target == high && in_storage + 1 == low
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs all necessary migrations based on `StorageVersion`.
|
||||
///
|
||||
/// If `TEST_ALL_STEPS == true` and `try-runtime` is enabled, this will run all the migrations
|
||||
/// inside `on_runtime_upgrade`. This should be set to false in tests that want to ensure the step
|
||||
/// by step migration works.
|
||||
pub struct Migration<T: Config, const TEST_ALL_STEPS: bool = true>(PhantomData<T>);
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
impl<T: Config, const TEST_ALL_STEPS: bool> Migration<T, TEST_ALL_STEPS> {
|
||||
fn run_all_steps() -> Result<(), TryRuntimeError> {
|
||||
let mut meter = &mut WeightMeter::new();
|
||||
let name = <Pallet<T>>::name();
|
||||
loop {
|
||||
let in_progress_version = <Pallet<T>>::on_chain_storage_version() + 1;
|
||||
let state = T::Migrations::pre_upgrade_step(in_progress_version)?;
|
||||
let before = meter.consumed();
|
||||
let status = Self::migrate(&mut meter);
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"{name}: Migration step {:?} weight = {}",
|
||||
in_progress_version,
|
||||
meter.consumed() - before
|
||||
);
|
||||
T::Migrations::post_upgrade_step(in_progress_version, state)?;
|
||||
if matches!(status, MigrateResult::Completed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let name = <Pallet<T>>::name();
|
||||
log::info!(target: LOG_TARGET, "{name}: Migration steps weight = {}", meter.consumed());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, const TEST_ALL_STEPS: bool> OnRuntimeUpgrade for Migration<T, TEST_ALL_STEPS> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let name = <Pallet<T>>::name();
|
||||
let in_code_version = <Pallet<T>>::in_code_storage_version();
|
||||
let on_chain_version = <Pallet<T>>::on_chain_storage_version();
|
||||
|
||||
if on_chain_version == in_code_version {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"{name}: No Migration performed storage_version = latest_version = {:?}",
|
||||
&on_chain_version
|
||||
);
|
||||
return T::WeightInfo::on_runtime_upgrade_noop();
|
||||
}
|
||||
|
||||
// In case a migration is already in progress we create the next migration
|
||||
// (if any) right when the current one finishes.
|
||||
if Self::in_progress() {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"{name}: Migration already in progress {:?}",
|
||||
&on_chain_version
|
||||
);
|
||||
|
||||
return T::WeightInfo::on_runtime_upgrade_in_progress();
|
||||
}
|
||||
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"{name}: Upgrading storage from {on_chain_version:?} to {in_code_version:?}.",
|
||||
);
|
||||
|
||||
let cursor = T::Migrations::new(on_chain_version + 1);
|
||||
MigrationInProgress::<T>::set(Some(cursor));
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
if TEST_ALL_STEPS {
|
||||
Self::run_all_steps().unwrap();
|
||||
}
|
||||
|
||||
T::WeightInfo::on_runtime_upgrade()
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
// We can't really do much here as our migrations do not happen during the runtime upgrade.
|
||||
// Instead, we call the migrations `pre_upgrade` and `post_upgrade` hooks when we iterate
|
||||
// over our migrations.
|
||||
let on_chain_version = <Pallet<T>>::on_chain_storage_version();
|
||||
let in_code_version = <Pallet<T>>::in_code_storage_version();
|
||||
|
||||
if on_chain_version == in_code_version {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Requested migration of {} from {:?}(on-chain storage version) to {:?}(in-code storage version)",
|
||||
<Pallet<T>>::name(), on_chain_version, in_code_version
|
||||
);
|
||||
|
||||
ensure!(
|
||||
T::Migrations::is_upgrade_supported(on_chain_version, in_code_version),
|
||||
"Unsupported upgrade: VERSION_RANGE should be (on-chain storage version + 1, in-code storage version)"
|
||||
);
|
||||
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
if !TEST_ALL_STEPS {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!(target: LOG_TARGET, "=== POST UPGRADE CHECKS ===");
|
||||
|
||||
// Ensure that the hashing algorithm is correct for each storage map.
|
||||
if let Some(hash) = crate::CodeInfoOf::<T>::iter_keys().next() {
|
||||
crate::CodeInfoOf::<T>::get(hash).expect("CodeInfo exists for hash; qed");
|
||||
}
|
||||
if let Some(hash) = crate::PristineCode::<T>::iter_keys().next() {
|
||||
crate::PristineCode::<T>::get(hash).expect("PristineCode exists for hash; qed");
|
||||
}
|
||||
if let Some(account_id) = crate::ContractInfoOf::<T>::iter_keys().next() {
|
||||
crate::ContractInfoOf::<T>::get(account_id)
|
||||
.expect("ContractInfo exists for account_id; qed");
|
||||
}
|
||||
if let Some(nonce) = crate::DeletionQueue::<T>::iter_keys().next() {
|
||||
crate::DeletionQueue::<T>::get(nonce).expect("DeletionQueue exists for nonce; qed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of running the migration.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum MigrateResult {
|
||||
/// No migration was performed
|
||||
NoMigrationPerformed,
|
||||
/// No migration currently in progress
|
||||
NoMigrationInProgress,
|
||||
/// A migration is in progress
|
||||
InProgress { steps_done: u32 },
|
||||
/// All migrations are completed
|
||||
Completed,
|
||||
}
|
||||
|
||||
/// The result of running a migration step.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum StepResult {
|
||||
InProgress { cursor: Cursor, steps_done: u32 },
|
||||
Completed { steps_done: u32 },
|
||||
}
|
||||
|
||||
impl<T: Config, const TEST_ALL_STEPS: bool> Migration<T, TEST_ALL_STEPS> {
|
||||
/// Verify that each migration's step of the [`Config::Migrations`] sequence fits into
|
||||
/// `Cursor`.
|
||||
pub(crate) fn integrity_test() {
|
||||
let max_weight = <T as pezframe_system::Config>::BlockWeights::get().max_block;
|
||||
T::Migrations::integrity_test(max_weight)
|
||||
}
|
||||
|
||||
/// Execute the multi-step migration.
|
||||
/// Returns whether or not a migration is in progress
|
||||
pub(crate) fn migrate(mut meter: &mut WeightMeter) -> MigrateResult {
|
||||
let name = <Pallet<T>>::name();
|
||||
|
||||
if meter.try_consume(T::WeightInfo::migrate()).is_err() {
|
||||
return MigrateResult::NoMigrationPerformed;
|
||||
}
|
||||
|
||||
MigrationInProgress::<T>::mutate_exists(|progress| {
|
||||
let Some(cursor_before) = progress.as_mut() else {
|
||||
meter.consume(T::WeightInfo::migration_noop());
|
||||
return MigrateResult::NoMigrationInProgress;
|
||||
};
|
||||
|
||||
// if a migration is running it is always upgrading to the next version
|
||||
let storage_version = <Pallet<T>>::on_chain_storage_version();
|
||||
let in_progress_version = storage_version + 1;
|
||||
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"{name}: Migrating from {:?} to {:?},",
|
||||
storage_version,
|
||||
in_progress_version,
|
||||
);
|
||||
|
||||
let result =
|
||||
match T::Migrations::steps(in_progress_version, cursor_before.as_ref(), &mut meter)
|
||||
{
|
||||
StepResult::InProgress { cursor, steps_done } => {
|
||||
*progress = Some(cursor);
|
||||
MigrateResult::InProgress { steps_done }
|
||||
},
|
||||
StepResult::Completed { steps_done } => {
|
||||
in_progress_version.put::<Pallet<T>>();
|
||||
if <Pallet<T>>::in_code_storage_version() != in_progress_version {
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"{name}: Next migration is {:?},",
|
||||
in_progress_version + 1
|
||||
);
|
||||
*progress = Some(T::Migrations::new(in_progress_version + 1));
|
||||
MigrateResult::InProgress { steps_done }
|
||||
} else {
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"{name}: All migrations done. At version {:?},",
|
||||
in_progress_version
|
||||
);
|
||||
*progress = None;
|
||||
MigrateResult::Completed
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_migrated() -> DispatchResult {
|
||||
if Self::in_progress() {
|
||||
Err(Error::<T>::MigrationInProgress.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn in_progress() -> bool {
|
||||
MigrationInProgress::<T>::exists()
|
||||
}
|
||||
}
|
||||
|
||||
#[impl_trait_for_tuples::impl_for_tuples(10)]
|
||||
#[tuple_types_custom_trait_bound(MigrationStep)]
|
||||
impl MigrateSequence for Tuple {
|
||||
const VERSION_RANGE: (u16, u16) = {
|
||||
let mut versions: (u16, u16) = (0, 0);
|
||||
for_tuples!(
|
||||
#(
|
||||
match versions {
|
||||
(0, 0) => {
|
||||
versions = (Tuple::VERSION, Tuple::VERSION);
|
||||
},
|
||||
(min_version, last_version) if Tuple::VERSION == last_version + 1 => {
|
||||
versions = (min_version, Tuple::VERSION);
|
||||
},
|
||||
_ => panic!("Migrations must be ordered by their versions with no gaps.")
|
||||
}
|
||||
)*
|
||||
);
|
||||
versions
|
||||
};
|
||||
|
||||
fn new(version: StorageVersion) -> Cursor {
|
||||
for_tuples!(
|
||||
#(
|
||||
if version == Tuple::VERSION {
|
||||
return Tuple::default().encode().try_into().expect(PROOF_ENCODE)
|
||||
}
|
||||
)*
|
||||
);
|
||||
invalid_version(version)
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
/// Execute the pre-checks of the step associated with this version.
|
||||
fn pre_upgrade_step(version: StorageVersion) -> Result<Vec<u8>, TryRuntimeError> {
|
||||
for_tuples!(
|
||||
#(
|
||||
if version == Tuple::VERSION {
|
||||
return Tuple::pre_upgrade_step()
|
||||
}
|
||||
)*
|
||||
);
|
||||
invalid_version(version)
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
/// Execute the post-checks of the step associated with this version.
|
||||
fn post_upgrade_step(version: StorageVersion, state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
for_tuples!(
|
||||
#(
|
||||
if version == Tuple::VERSION {
|
||||
return Tuple::post_upgrade_step(state)
|
||||
}
|
||||
)*
|
||||
);
|
||||
invalid_version(version)
|
||||
}
|
||||
|
||||
fn steps(version: StorageVersion, mut cursor: &[u8], meter: &mut WeightMeter) -> StepResult {
|
||||
for_tuples!(
|
||||
#(
|
||||
if version == Tuple::VERSION {
|
||||
let mut migration = <Tuple as Decode>::decode(&mut cursor)
|
||||
.expect(PROOF_DECODE);
|
||||
let max_weight = Tuple::max_step_weight();
|
||||
let mut steps_done = 0;
|
||||
while meter.can_consume(max_weight) {
|
||||
steps_done.saturating_accrue(1);
|
||||
if matches!(migration.step(meter), IsFinished::Yes) {
|
||||
return StepResult::Completed{ steps_done }
|
||||
}
|
||||
}
|
||||
return StepResult::InProgress{cursor: migration.encode().try_into().expect(PROOF_ENCODE), steps_done }
|
||||
}
|
||||
)*
|
||||
);
|
||||
invalid_version(version)
|
||||
}
|
||||
|
||||
fn integrity_test(max_block_weight: Weight) {
|
||||
for_tuples!(
|
||||
#(
|
||||
Tuple::integrity_test(max_block_weight);
|
||||
)*
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
migration::codegen::LATEST_MIGRATION_VERSION,
|
||||
tests::{ExtBuilder, Test},
|
||||
};
|
||||
|
||||
#[derive(Default, Encode, Decode, MaxEncodedLen)]
|
||||
struct MockMigration<const N: u16> {
|
||||
// MockMigration<N> needs `N` steps to finish
|
||||
count: u16,
|
||||
}
|
||||
|
||||
impl<const N: u16> MigrationStep for MockMigration<N> {
|
||||
const VERSION: u16 = N;
|
||||
fn max_step_weight() -> Weight {
|
||||
Weight::from_all(1)
|
||||
}
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
assert!(self.count != N);
|
||||
self.count += 1;
|
||||
meter.consume(Weight::from_all(1));
|
||||
if self.count == N {
|
||||
IsFinished::Yes
|
||||
} else {
|
||||
IsFinished::No
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_storage_version_matches_last_migration_file() {
|
||||
assert_eq!(StorageVersion::new(LATEST_MIGRATION_VERSION), crate::pallet::STORAGE_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_range_works() {
|
||||
let range = <(MockMigration<1>, MockMigration<2>)>::VERSION_RANGE;
|
||||
assert_eq!(range, (1, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_upgrade_supported_works() {
|
||||
type Migrations = (MockMigration<9>, MockMigration<10>, MockMigration<11>);
|
||||
assert!(Migrations::is_upgrade_supported(StorageVersion::new(8), StorageVersion::new(11)));
|
||||
assert!(!Migrations::is_upgrade_supported(StorageVersion::new(9), StorageVersion::new(11)));
|
||||
assert!(!Migrations::is_upgrade_supported(StorageVersion::new(8), StorageVersion::new(12)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steps_works() {
|
||||
type Migrations = (MockMigration<2>, MockMigration<3>);
|
||||
let version = StorageVersion::new(2);
|
||||
let mut cursor = Migrations::new(version);
|
||||
|
||||
let mut meter = WeightMeter::with_limit(Weight::from_all(1));
|
||||
let result = Migrations::steps(version, &cursor, &mut meter);
|
||||
cursor = alloc::vec![1u8, 0].try_into().unwrap();
|
||||
assert_eq!(result, StepResult::InProgress { cursor: cursor.clone(), steps_done: 1 });
|
||||
assert_eq!(meter.consumed(), Weight::from_all(1));
|
||||
|
||||
let mut meter = WeightMeter::with_limit(Weight::from_all(1));
|
||||
assert_eq!(
|
||||
Migrations::steps(version, &cursor, &mut meter),
|
||||
StepResult::Completed { steps_done: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_migration_in_progress_works() {
|
||||
type TestMigration = Migration<Test>;
|
||||
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
assert_eq!(StorageVersion::get::<Pallet<Test>>(), LATEST_MIGRATION_VERSION);
|
||||
assert_eq!(
|
||||
TestMigration::migrate(&mut WeightMeter::new()),
|
||||
MigrateResult::NoMigrationInProgress
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_works() {
|
||||
type TestMigration = Migration<Test, false>;
|
||||
|
||||
ExtBuilder::default()
|
||||
.set_storage_version(LATEST_MIGRATION_VERSION - 2)
|
||||
.build()
|
||||
.execute_with(|| {
|
||||
assert_eq!(StorageVersion::get::<Pallet<Test>>(), LATEST_MIGRATION_VERSION - 2);
|
||||
TestMigration::on_runtime_upgrade();
|
||||
for (version, status) in [
|
||||
(LATEST_MIGRATION_VERSION - 1, MigrateResult::InProgress { steps_done: 1 }),
|
||||
(LATEST_MIGRATION_VERSION, MigrateResult::Completed),
|
||||
] {
|
||||
assert_eq!(TestMigration::migrate(&mut WeightMeter::new()), status);
|
||||
assert_eq!(
|
||||
<Pallet<Test>>::on_chain_storage_version(),
|
||||
StorageVersion::new(version)
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
TestMigration::migrate(&mut WeightMeter::new()),
|
||||
MigrateResult::NoMigrationInProgress
|
||||
);
|
||||
assert_eq!(StorageVersion::get::<Pallet<Test>>(), LATEST_MIGRATION_VERSION);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Update `CodeStorage` with the new `determinism` field.
|
||||
|
||||
use crate::{
|
||||
migration::{IsFinished, MigrationStep},
|
||||
weights::WeightInfo,
|
||||
CodeHash, Config, Determinism, Pallet, Weight, LOG_TARGET,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
use pezframe_support::{
|
||||
pezpallet_prelude::*, storage_alias, weights::WeightMeter, DefaultNoBound, Identity,
|
||||
};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
mod v8 {
|
||||
use super::*;
|
||||
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct PrefabWasmModule {
|
||||
#[codec(compact)]
|
||||
pub instruction_weights_version: u32,
|
||||
#[codec(compact)]
|
||||
pub initial: u32,
|
||||
#[codec(compact)]
|
||||
pub maximum: u32,
|
||||
pub code: Vec<u8>,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type CodeStorage<T: Config> =
|
||||
StorageMap<Pallet<T>, Identity, CodeHash<T>, PrefabWasmModule>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn store_old_dummy_code<T: Config>(len: usize) {
|
||||
use pezsp_runtime::traits::Hash;
|
||||
let module = v8::PrefabWasmModule {
|
||||
instruction_weights_version: 0,
|
||||
initial: 0,
|
||||
maximum: 0,
|
||||
code: alloc::vec![42u8; len],
|
||||
};
|
||||
let hash = T::Hashing::hash(&module.code);
|
||||
v8::CodeStorage::<T>::insert(hash, module);
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode)]
|
||||
struct PrefabWasmModule {
|
||||
#[codec(compact)]
|
||||
pub instruction_weights_version: u32,
|
||||
#[codec(compact)]
|
||||
pub initial: u32,
|
||||
#[codec(compact)]
|
||||
pub maximum: u32,
|
||||
pub code: Vec<u8>,
|
||||
pub determinism: Determinism,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
type CodeStorage<T: Config> = StorageMap<Pallet<T>, Identity, CodeHash<T>, PrefabWasmModule>;
|
||||
|
||||
#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
|
||||
pub struct Migration<T: Config> {
|
||||
last_code_hash: Option<CodeHash<T>>,
|
||||
}
|
||||
|
||||
impl<T: Config> MigrationStep for Migration<T> {
|
||||
const VERSION: u16 = 9;
|
||||
|
||||
fn max_step_weight() -> Weight {
|
||||
T::WeightInfo::v9_migration_step(T::MaxCodeLen::get())
|
||||
}
|
||||
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
let mut iter = if let Some(last_key) = self.last_code_hash.take() {
|
||||
v8::CodeStorage::<T>::iter_from(v8::CodeStorage::<T>::hashed_key_for(last_key))
|
||||
} else {
|
||||
v8::CodeStorage::<T>::iter()
|
||||
};
|
||||
|
||||
if let Some((key, old)) = iter.next() {
|
||||
log::debug!(target: LOG_TARGET, "Migrating contract code {:?}", key);
|
||||
let len = old.code.len() as u32;
|
||||
let module = PrefabWasmModule {
|
||||
instruction_weights_version: old.instruction_weights_version,
|
||||
initial: old.initial,
|
||||
maximum: old.maximum,
|
||||
code: old.code,
|
||||
determinism: Determinism::Enforced,
|
||||
};
|
||||
CodeStorage::<T>::insert(key, module);
|
||||
self.last_code_hash = Some(key);
|
||||
meter.consume(T::WeightInfo::v9_migration_step(len));
|
||||
IsFinished::No
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "No more contracts code to migrate");
|
||||
meter.consume(T::WeightInfo::v9_migration_step(0));
|
||||
IsFinished::Yes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade_step() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
let sample: Vec<_> = v8::CodeStorage::<T>::iter().take(100).collect();
|
||||
|
||||
log::debug!(target: LOG_TARGET, "Taking sample of {} contract codes", sample.len());
|
||||
Ok(sample.encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let sample = <Vec<(CodeHash<T>, v8::PrefabWasmModule)> as Decode>::decode(&mut &state[..])
|
||||
.expect("pre_upgrade_step provides a valid state; qed");
|
||||
|
||||
log::debug!(target: LOG_TARGET, "Validating sample of {} contract codes", sample.len());
|
||||
for (code_hash, old) in sample {
|
||||
let module = CodeStorage::<T>::get(&code_hash).unwrap();
|
||||
ensure!(
|
||||
module.instruction_weights_version == old.instruction_weights_version,
|
||||
"invalid instruction weights version"
|
||||
);
|
||||
ensure!(module.determinism == Determinism::Enforced, "invalid determinism");
|
||||
ensure!(module.initial == old.initial, "invalid initial");
|
||||
ensure!(module.maximum == old.maximum, "invalid maximum");
|
||||
ensure!(module.code == old.code, "invalid code");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Don't rely on reserved balances keeping an account alive
|
||||
//! See <https://github.com/pezkuwichain/kurdistan-sdk/issues/44>.
|
||||
|
||||
use crate::{
|
||||
exec::AccountIdOf,
|
||||
migration::{IsFinished, MigrationStep},
|
||||
weights::WeightInfo,
|
||||
CodeHash, Config, Pallet, TrieId, Weight, LOG_TARGET,
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
use core::{
|
||||
cmp::{max, min},
|
||||
ops::Deref,
|
||||
};
|
||||
use pezframe_support::{
|
||||
pezpallet_prelude::*,
|
||||
storage_alias,
|
||||
traits::{
|
||||
tokens::{fungible::Inspect, Fortitude::Polite, Preservation::Preserve},
|
||||
ExistenceRequirement, ReservableCurrency,
|
||||
},
|
||||
weights::WeightMeter,
|
||||
DefaultNoBound,
|
||||
};
|
||||
use pezsp_core::hexdisplay::HexDisplay;
|
||||
use pezsp_runtime::{
|
||||
traits::{Hash, TrailingZeroInput, Zero},
|
||||
Perbill, Saturating,
|
||||
};
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
mod v9 {
|
||||
use super::*;
|
||||
|
||||
pub type BalanceOf<T, OldCurrency> = <OldCurrency as pezframe_support::traits::Currency<
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
>>::Balance;
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T, OldCurrency))]
|
||||
pub struct ContractInfo<T: Config, OldCurrency>
|
||||
where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
{
|
||||
pub trie_id: TrieId,
|
||||
pub code_hash: CodeHash<T>,
|
||||
pub storage_bytes: u32,
|
||||
pub storage_items: u32,
|
||||
pub storage_byte_deposit: BalanceOf<T, OldCurrency>,
|
||||
pub storage_item_deposit: BalanceOf<T, OldCurrency>,
|
||||
pub storage_base_deposit: BalanceOf<T, OldCurrency>,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type ContractInfoOf<T: Config, OldCurrency> = StorageMap<
|
||||
Pallet<T>,
|
||||
Twox64Concat,
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
ContractInfo<T, OldCurrency>,
|
||||
>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn store_old_contract_info<T: Config, OldCurrency>(
|
||||
account: T::AccountId,
|
||||
info: crate::ContractInfo<T>,
|
||||
) where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId> + 'static,
|
||||
{
|
||||
let info = v9::ContractInfo {
|
||||
trie_id: info.trie_id,
|
||||
code_hash: info.code_hash,
|
||||
storage_bytes: Default::default(),
|
||||
storage_items: Default::default(),
|
||||
storage_byte_deposit: Default::default(),
|
||||
storage_item_deposit: Default::default(),
|
||||
storage_base_deposit: Default::default(),
|
||||
};
|
||||
v9::ContractInfoOf::<T, OldCurrency>::insert(account, info);
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct DepositAccount<T: Config>(AccountIdOf<T>);
|
||||
|
||||
impl<T: Config> Deref for DepositAccount<T> {
|
||||
type Target = AccountIdOf<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T, OldCurrency))]
|
||||
pub struct ContractInfo<T: Config, OldCurrency>
|
||||
where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
{
|
||||
pub trie_id: TrieId,
|
||||
deposit_account: DepositAccount<T>,
|
||||
pub code_hash: CodeHash<T>,
|
||||
storage_bytes: u32,
|
||||
storage_items: u32,
|
||||
pub storage_byte_deposit: v9::BalanceOf<T, OldCurrency>,
|
||||
storage_item_deposit: v9::BalanceOf<T, OldCurrency>,
|
||||
storage_base_deposit: v9::BalanceOf<T, OldCurrency>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
|
||||
pub struct Migration<T: Config, OldCurrency = ()> {
|
||||
last_account: Option<T::AccountId>,
|
||||
_phantom: PhantomData<(T, OldCurrency)>,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
type ContractInfoOf<T: Config, OldCurrency> = StorageMap<
|
||||
Pallet<T>,
|
||||
Twox64Concat,
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
ContractInfo<T, OldCurrency>,
|
||||
>;
|
||||
|
||||
/// Formula: `hash("contract_depo_v1" ++ contract_addr)`
|
||||
fn deposit_address<T: Config>(
|
||||
contract_addr: &<T as pezframe_system::Config>::AccountId,
|
||||
) -> <T as pezframe_system::Config>::AccountId {
|
||||
let entropy = (b"contract_depo_v1", contract_addr)
|
||||
.using_encoded(<T as pezframe_system::Config>::Hashing::hash);
|
||||
Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
|
||||
.expect("infinite length input; no invalid inputs for type; qed")
|
||||
}
|
||||
|
||||
impl<T: Config, OldCurrency: 'static> MigrationStep for Migration<T, OldCurrency>
|
||||
where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>
|
||||
+ Inspect<<T as pezframe_system::Config>::AccountId, Balance = v9::BalanceOf<T, OldCurrency>>,
|
||||
{
|
||||
const VERSION: u16 = 10;
|
||||
|
||||
fn max_step_weight() -> Weight {
|
||||
T::WeightInfo::v10_migration_step()
|
||||
}
|
||||
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
let mut iter = if let Some(last_account) = self.last_account.take() {
|
||||
v9::ContractInfoOf::<T, OldCurrency>::iter_from(
|
||||
v9::ContractInfoOf::<T, OldCurrency>::hashed_key_for(last_account),
|
||||
)
|
||||
} else {
|
||||
v9::ContractInfoOf::<T, OldCurrency>::iter()
|
||||
};
|
||||
|
||||
if let Some((account, contract)) = iter.next() {
|
||||
let min_balance = <OldCurrency as pezframe_support::traits::Currency<
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
>>::minimum_balance();
|
||||
log::debug!(target: LOG_TARGET, "Account: 0x{} ", HexDisplay::from(&account.encode()));
|
||||
|
||||
// Get the new deposit account address
|
||||
let deposit_account: DepositAccount<T> = DepositAccount(deposit_address::<T>(&account));
|
||||
|
||||
// Calculate the existing deposit, that should be reserved on the contract account
|
||||
let old_deposit = contract
|
||||
.storage_base_deposit
|
||||
.saturating_add(contract.storage_item_deposit)
|
||||
.saturating_add(contract.storage_byte_deposit);
|
||||
|
||||
// Unreserve the existing deposit
|
||||
// Note we can't use repatriate_reserve, because it only works with existing accounts
|
||||
let remaining = OldCurrency::unreserve(&account, old_deposit);
|
||||
if !remaining.is_zero() {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"Partially unreserved. Remaining {:?} out of {:?} asked",
|
||||
remaining,
|
||||
old_deposit
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt to transfer the old deposit to the deposit account.
|
||||
let amount = old_deposit
|
||||
.saturating_sub(min_balance)
|
||||
.min(OldCurrency::reducible_balance(&account, Preserve, Polite));
|
||||
|
||||
let new_deposit = OldCurrency::transfer(
|
||||
&account,
|
||||
&deposit_account,
|
||||
amount,
|
||||
ExistenceRequirement::KeepAlive,
|
||||
)
|
||||
.map(|_| {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Transferred deposit ({:?}) to deposit account",
|
||||
amount
|
||||
);
|
||||
amount
|
||||
})
|
||||
// If it fails we fallback to minting the ED.
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to transfer the base deposit, reason: {:?}",
|
||||
err
|
||||
);
|
||||
let _ = OldCurrency::deposit_creating(&deposit_account, min_balance);
|
||||
min_balance
|
||||
});
|
||||
|
||||
// Calculate the new base_deposit to store in the contract:
|
||||
// Ideally, it should be the same as the old one
|
||||
// Ideally, it should be at least 2xED (for the contract and deposit accounts).
|
||||
// It can't be more than the `new_deposit`.
|
||||
let new_base_deposit = min(
|
||||
max(contract.storage_base_deposit, min_balance.saturating_add(min_balance)),
|
||||
new_deposit,
|
||||
);
|
||||
|
||||
// Calculate the ratio to adjust storage_byte and storage_item deposits.
|
||||
let new_deposit_without_base = new_deposit.saturating_sub(new_base_deposit);
|
||||
let old_deposit_without_base =
|
||||
old_deposit.saturating_sub(contract.storage_base_deposit);
|
||||
let ratio = Perbill::from_rational(new_deposit_without_base, old_deposit_without_base);
|
||||
|
||||
// Calculate the new storage deposits based on the ratio
|
||||
let storage_byte_deposit = ratio.mul_ceil(contract.storage_byte_deposit);
|
||||
let storage_item_deposit = ratio.mul_ceil(contract.storage_item_deposit);
|
||||
|
||||
// Recalculate the new base deposit, instead of using new_base_deposit to avoid rounding
|
||||
// errors
|
||||
let storage_base_deposit = new_deposit
|
||||
.saturating_sub(storage_byte_deposit)
|
||||
.saturating_sub(storage_item_deposit);
|
||||
|
||||
let new_contract_info = ContractInfo {
|
||||
trie_id: contract.trie_id,
|
||||
deposit_account,
|
||||
code_hash: contract.code_hash,
|
||||
storage_bytes: contract.storage_bytes,
|
||||
storage_items: contract.storage_items,
|
||||
storage_byte_deposit,
|
||||
storage_item_deposit,
|
||||
storage_base_deposit,
|
||||
};
|
||||
|
||||
ContractInfoOf::<T, OldCurrency>::insert(&account, new_contract_info);
|
||||
|
||||
// Store last key for next migration step
|
||||
self.last_account = Some(account);
|
||||
|
||||
meter.consume(T::WeightInfo::v10_migration_step());
|
||||
IsFinished::No
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "Done Migrating contract info");
|
||||
meter.consume(T::WeightInfo::v10_migration_step());
|
||||
IsFinished::Yes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade_step() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
let sample: Vec<_> = v9::ContractInfoOf::<T, OldCurrency>::iter().take(10).collect();
|
||||
|
||||
log::debug!(target: LOG_TARGET, "Taking sample of {} contracts", sample.len());
|
||||
Ok(sample.encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let sample = <Vec<(T::AccountId, v9::ContractInfo<T, OldCurrency>)> as Decode>::decode(
|
||||
&mut &state[..],
|
||||
)
|
||||
.expect("pre_upgrade_step provides a valid state; qed");
|
||||
|
||||
log::debug!(target: LOG_TARGET, "Validating sample of {} contracts", sample.len());
|
||||
for (account, old_contract) in sample {
|
||||
log::debug!(target: LOG_TARGET, "===");
|
||||
log::debug!(target: LOG_TARGET, "Account: 0x{} ", HexDisplay::from(&account.encode()));
|
||||
let contract = ContractInfoOf::<T, OldCurrency>::get(&account).unwrap();
|
||||
ensure!(old_contract.trie_id == contract.trie_id, "invalid trie_id");
|
||||
ensure!(old_contract.code_hash == contract.code_hash, "invalid code_hash");
|
||||
ensure!(old_contract.storage_bytes == contract.storage_bytes, "invalid storage_bytes");
|
||||
ensure!(old_contract.storage_items == contract.storage_items, "invalid storage_items");
|
||||
|
||||
let deposit = <OldCurrency as pezframe_support::traits::Currency<_>>::total_balance(
|
||||
&contract.deposit_account,
|
||||
);
|
||||
ensure!(
|
||||
deposit ==
|
||||
contract
|
||||
.storage_base_deposit
|
||||
.saturating_add(contract.storage_item_deposit)
|
||||
.saturating_add(contract.storage_byte_deposit),
|
||||
"deposit mismatch"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Overflowing bounded DeletionQueue.
|
||||
//! See <https://github.com/pezkuwichain/kurdistan-sdk/issues/47>.
|
||||
|
||||
use crate::{
|
||||
migration::{IsFinished, MigrationStep},
|
||||
weights::WeightInfo,
|
||||
Config, Pallet, TrieId, Weight, LOG_TARGET,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
use core::marker::PhantomData;
|
||||
use pezframe_support::{pezpallet_prelude::*, storage_alias, weights::WeightMeter, DefaultNoBound};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
mod v10 {
|
||||
use super::*;
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct DeletedContract {
|
||||
pub(crate) trie_id: TrieId,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type DeletionQueue<T: Config> = StorageValue<Pallet<T>, Vec<DeletedContract>>;
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, DefaultNoBound, Clone)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct DeletionQueueManager<T: Config> {
|
||||
insert_counter: u32,
|
||||
delete_counter: u32,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", feature = "try-runtime"))]
|
||||
pub fn fill_old_queue<T: Config>(len: usize) {
|
||||
let queue: Vec<v10::DeletedContract> =
|
||||
core::iter::repeat_with(|| v10::DeletedContract { trie_id: Default::default() })
|
||||
.take(len)
|
||||
.collect();
|
||||
v10::DeletionQueue::<T>::set(Some(queue));
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
type DeletionQueue<T: Config> = StorageMap<Pallet<T>, Twox64Concat, u32, TrieId>;
|
||||
|
||||
#[storage_alias]
|
||||
type DeletionQueueCounter<T: Config> = StorageValue<Pallet<T>, DeletionQueueManager<T>, ValueQuery>;
|
||||
|
||||
#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
|
||||
pub struct Migration<T: Config> {
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> MigrationStep for Migration<T> {
|
||||
const VERSION: u16 = 11;
|
||||
|
||||
// It would be more correct to make our use the now removed [DeletionQueueDepth](https://github.com/pezkuwichain/kurdistan-sdk/issues/47/files#diff-70e9723e9db62816e35f6f885b6770a8449c75a6c2733e9fa7a245fe52c4656c)
|
||||
// but in practice the queue is always empty, so 128 is a good enough approximation for not
|
||||
// underestimating the weight of our migration.
|
||||
fn max_step_weight() -> Weight {
|
||||
T::WeightInfo::v11_migration_step(128)
|
||||
}
|
||||
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
let Some(old_queue) = v10::DeletionQueue::<T>::take() else {
|
||||
meter.consume(T::WeightInfo::v11_migration_step(0));
|
||||
return IsFinished::Yes;
|
||||
};
|
||||
let len = old_queue.len();
|
||||
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Migrating deletion queue with {} deleted contracts",
|
||||
old_queue.len()
|
||||
);
|
||||
|
||||
if !old_queue.is_empty() {
|
||||
let mut queue = DeletionQueueManager::<T>::default();
|
||||
for contract in old_queue {
|
||||
<DeletionQueue<T>>::insert(queue.insert_counter, contract.trie_id);
|
||||
queue.insert_counter += 1;
|
||||
}
|
||||
|
||||
<DeletionQueueCounter<T>>::set(queue);
|
||||
}
|
||||
|
||||
meter.consume(T::WeightInfo::v11_migration_step(len as u32));
|
||||
IsFinished::Yes
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade_step() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
let old_queue = v10::DeletionQueue::<T>::take().unwrap_or_default();
|
||||
|
||||
if old_queue.is_empty() {
|
||||
let len = 10u32;
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Injecting {len} entries to deletion queue to test migration"
|
||||
);
|
||||
fill_old_queue::<T>(len as usize);
|
||||
return Ok(len.encode());
|
||||
}
|
||||
|
||||
Ok((old_queue.len() as u32).encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let len = <u32 as Decode>::decode(&mut &state[..])
|
||||
.expect("pre_upgrade_step provides a valid state; qed");
|
||||
let counter = <DeletionQueueCounter<T>>::get();
|
||||
ensure!(counter.insert_counter == len, "invalid insert counter");
|
||||
ensure!(counter.delete_counter == 0, "invalid delete counter");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Move `OwnerInfo` to `CodeInfo`, add `determinism` field to the latter, clear `CodeStorage` and
|
||||
//! repay deposits.
|
||||
|
||||
use crate::{
|
||||
migration::{IsFinished, MigrationStep},
|
||||
weights::WeightInfo,
|
||||
AccountIdOf, BalanceOf, CodeHash, Config, Determinism, Pallet, Weight, LOG_TARGET,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
use pezframe_support::{
|
||||
pezpallet_prelude::*, storage_alias, traits::ReservableCurrency, weights::WeightMeter,
|
||||
DefaultNoBound, Identity,
|
||||
};
|
||||
use scale_info::prelude::format;
|
||||
use pezsp_core::hexdisplay::HexDisplay;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
use pezsp_runtime::{traits::Zero, FixedPointNumber, FixedU128, Saturating};
|
||||
|
||||
mod v11 {
|
||||
use super::*;
|
||||
|
||||
pub type BalanceOf<T, OldCurrency> = <OldCurrency as pezframe_support::traits::Currency<
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
>>::Balance;
|
||||
|
||||
#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
|
||||
#[codec(mel_bound())]
|
||||
#[scale_info(skip_type_params(T, OldCurrency))]
|
||||
pub struct OwnerInfo<T: Config, OldCurrency>
|
||||
where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
{
|
||||
pub owner: AccountIdOf<T>,
|
||||
#[codec(compact)]
|
||||
pub deposit: BalanceOf<T, OldCurrency>,
|
||||
#[codec(compact)]
|
||||
pub refcount: u64,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, scale_info::TypeInfo)]
|
||||
#[codec(mel_bound())]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct PrefabWasmModule {
|
||||
#[codec(compact)]
|
||||
pub instruction_weights_version: u32,
|
||||
#[codec(compact)]
|
||||
pub initial: u32,
|
||||
#[codec(compact)]
|
||||
pub maximum: u32,
|
||||
pub code: Vec<u8>,
|
||||
pub determinism: Determinism,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type OwnerInfoOf<T: Config, OldCurrency> =
|
||||
StorageMap<Pallet<T>, Identity, CodeHash<T>, OwnerInfo<T, OldCurrency>>;
|
||||
|
||||
#[storage_alias]
|
||||
pub type CodeStorage<T: Config> =
|
||||
StorageMap<Pallet<T>, Identity, CodeHash<T>, PrefabWasmModule>;
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
|
||||
#[codec(mel_bound())]
|
||||
#[scale_info(skip_type_params(T, OldCurrency))]
|
||||
pub struct CodeInfo<T: Config, OldCurrency>
|
||||
where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
{
|
||||
owner: AccountIdOf<T>,
|
||||
#[codec(compact)]
|
||||
deposit: v11::BalanceOf<T, OldCurrency>,
|
||||
#[codec(compact)]
|
||||
refcount: u64,
|
||||
determinism: Determinism,
|
||||
code_len: u32,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type CodeInfoOf<T: Config, OldCurrency> =
|
||||
StorageMap<Pallet<T>, Identity, CodeHash<T>, CodeInfo<T, OldCurrency>>;
|
||||
|
||||
#[storage_alias]
|
||||
pub type PristineCode<T: Config> = StorageMap<Pallet<T>, Identity, CodeHash<T>, Vec<u8>>;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn store_old_dummy_code<T: Config, OldCurrency>(len: usize, account: T::AccountId)
|
||||
where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId> + 'static,
|
||||
{
|
||||
use pezsp_runtime::traits::Hash;
|
||||
|
||||
let code = alloc::vec![42u8; len];
|
||||
let hash = T::Hashing::hash(&code);
|
||||
PristineCode::<T>::insert(hash, code.clone());
|
||||
|
||||
let module = v11::PrefabWasmModule {
|
||||
instruction_weights_version: Default::default(),
|
||||
initial: Default::default(),
|
||||
maximum: Default::default(),
|
||||
code,
|
||||
determinism: Determinism::Enforced,
|
||||
};
|
||||
v11::CodeStorage::<T>::insert(hash, module);
|
||||
|
||||
let info = v11::OwnerInfo { owner: account, deposit: u32::MAX.into(), refcount: u64::MAX };
|
||||
v11::OwnerInfoOf::<T, OldCurrency>::insert(hash, info);
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
|
||||
pub struct Migration<T: Config, OldCurrency>
|
||||
where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
OldCurrency::Balance: From<BalanceOf<T>>,
|
||||
{
|
||||
last_code_hash: Option<CodeHash<T>>,
|
||||
_phantom: PhantomData<OldCurrency>,
|
||||
}
|
||||
|
||||
impl<T: Config, OldCurrency> MigrationStep for Migration<T, OldCurrency>
|
||||
where
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId> + 'static,
|
||||
OldCurrency::Balance: From<BalanceOf<T>>,
|
||||
{
|
||||
const VERSION: u16 = 12;
|
||||
|
||||
fn max_step_weight() -> Weight {
|
||||
T::WeightInfo::v12_migration_step(T::MaxCodeLen::get())
|
||||
}
|
||||
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
let mut iter = if let Some(last_key) = self.last_code_hash.take() {
|
||||
v11::OwnerInfoOf::<T, OldCurrency>::iter_from(
|
||||
v11::OwnerInfoOf::<T, OldCurrency>::hashed_key_for(last_key),
|
||||
)
|
||||
} else {
|
||||
v11::OwnerInfoOf::<T, OldCurrency>::iter()
|
||||
};
|
||||
if let Some((hash, old_info)) = iter.next() {
|
||||
log::debug!(target: LOG_TARGET, "Migrating OwnerInfo for code_hash {:?}", hash);
|
||||
|
||||
let module = v11::CodeStorage::<T>::take(hash)
|
||||
.expect(format!("No PrefabWasmModule found for code_hash: {:?}", hash).as_str());
|
||||
|
||||
let code_len = module.code.len();
|
||||
// We print this to measure the impact of the migration.
|
||||
// Storage removed: deleted PrefabWasmModule's encoded len.
|
||||
// Storage added: determinism field encoded len (as all other CodeInfo fields are the
|
||||
// same as in the deleted OwnerInfo).
|
||||
log::debug!(target: LOG_TARGET, "Storage removed: 1 item, {} bytes", &code_len,);
|
||||
|
||||
// Storage usage prices could change over time, and accounts who uploaded their
|
||||
// contracts code before the storage deposits where introduced, had not been ever
|
||||
// charged with any deposit for that (see migration v6).
|
||||
//
|
||||
// This is why deposit to be refunded here is calculated as follows:
|
||||
//
|
||||
// 1. Calculate the deposit amount for storage before the migration, given current
|
||||
// prices.
|
||||
// 2. Given current reserved deposit amount, calculate the correction factor.
|
||||
// 3. Calculate the deposit amount for storage after the migration, given current
|
||||
// prices.
|
||||
// 4. Calculate real deposit amount to be reserved after the migration.
|
||||
let price_per_byte = T::DepositPerByte::get();
|
||||
let price_per_item = T::DepositPerItem::get();
|
||||
let bytes_before = module
|
||||
.encoded_size()
|
||||
.saturating_add(code_len)
|
||||
.saturating_add(v11::OwnerInfo::<T, OldCurrency>::max_encoded_len())
|
||||
as u32;
|
||||
let items_before = 3u32;
|
||||
let deposit_expected_before = price_per_byte
|
||||
.saturating_mul(bytes_before.into())
|
||||
.saturating_add(price_per_item.saturating_mul(items_before.into()));
|
||||
let ratio = FixedU128::checked_from_rational(old_info.deposit, deposit_expected_before)
|
||||
.unwrap_or_default()
|
||||
.min(FixedU128::from_u32(1));
|
||||
let bytes_after =
|
||||
code_len.saturating_add(CodeInfo::<T, OldCurrency>::max_encoded_len()) as u32;
|
||||
let items_after = 2u32;
|
||||
let deposit_expected_after = price_per_byte
|
||||
.saturating_mul(bytes_after.into())
|
||||
.saturating_add(price_per_item.saturating_mul(items_after.into()));
|
||||
let deposit = ratio.saturating_mul_int(deposit_expected_after);
|
||||
|
||||
let info = CodeInfo::<T, OldCurrency> {
|
||||
determinism: module.determinism,
|
||||
owner: old_info.owner,
|
||||
deposit: deposit.into(),
|
||||
refcount: old_info.refcount,
|
||||
code_len: code_len as u32,
|
||||
};
|
||||
|
||||
let amount = old_info.deposit.saturating_sub(info.deposit);
|
||||
if !amount.is_zero() {
|
||||
OldCurrency::unreserve(&info.owner, amount);
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Deposit refunded: {:?} Balance, to: {:?}",
|
||||
&amount,
|
||||
HexDisplay::from(&info.owner.encode())
|
||||
);
|
||||
} else {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"new deposit: {:?} >= old deposit: {:?}",
|
||||
&info.deposit,
|
||||
&old_info.deposit
|
||||
);
|
||||
}
|
||||
CodeInfoOf::<T, OldCurrency>::insert(hash, info);
|
||||
|
||||
self.last_code_hash = Some(hash);
|
||||
|
||||
meter.consume(T::WeightInfo::v12_migration_step(code_len as u32));
|
||||
IsFinished::No
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "No more OwnerInfo to migrate");
|
||||
meter.consume(T::WeightInfo::v12_migration_step(0));
|
||||
IsFinished::Yes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade_step() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
let len = 100;
|
||||
log::debug!(target: LOG_TARGET, "Taking sample of {} OwnerInfo(s)", len);
|
||||
let sample: Vec<_> = v11::OwnerInfoOf::<T, OldCurrency>::iter()
|
||||
.take(len)
|
||||
.map(|(k, v)| {
|
||||
let module = v11::CodeStorage::<T>::get(k)
|
||||
.expect("No PrefabWasmModule found for code_hash: {:?}");
|
||||
let info: CodeInfo<T, OldCurrency> = CodeInfo {
|
||||
determinism: module.determinism,
|
||||
deposit: v.deposit,
|
||||
refcount: v.refcount,
|
||||
owner: v.owner,
|
||||
code_len: module.code.len() as u32,
|
||||
};
|
||||
(k, info)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let storage: u32 =
|
||||
v11::CodeStorage::<T>::iter().map(|(_k, v)| v.encoded_size() as u32).sum();
|
||||
let mut deposit: v11::BalanceOf<T, OldCurrency> = Default::default();
|
||||
v11::OwnerInfoOf::<T, OldCurrency>::iter().for_each(|(_k, v)| deposit += v.deposit);
|
||||
|
||||
Ok((sample, deposit, storage).encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let state = <(
|
||||
Vec<(CodeHash<T>, CodeInfo<T, OldCurrency>)>,
|
||||
v11::BalanceOf<T, OldCurrency>,
|
||||
u32,
|
||||
) as Decode>::decode(&mut &state[..])
|
||||
.unwrap();
|
||||
|
||||
log::debug!(target: LOG_TARGET, "Validating state of {} Codeinfo(s)", state.0.len());
|
||||
for (hash, old) in state.0 {
|
||||
let info = CodeInfoOf::<T, OldCurrency>::get(&hash)
|
||||
.expect(format!("CodeInfo for code_hash {:?} not found!", hash).as_str());
|
||||
ensure!(info.determinism == old.determinism, "invalid determinism");
|
||||
ensure!(info.owner == old.owner, "invalid owner");
|
||||
ensure!(info.refcount == old.refcount, "invalid refcount");
|
||||
}
|
||||
|
||||
if let Some((k, _)) = v11::CodeStorage::<T>::iter().next() {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"CodeStorage is still NOT empty, found code_hash: {:?}",
|
||||
k
|
||||
);
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "CodeStorage is empty.");
|
||||
}
|
||||
if let Some((k, _)) = v11::OwnerInfoOf::<T, OldCurrency>::iter().next() {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"OwnerInfoOf is still NOT empty, found code_hash: {:?}",
|
||||
k
|
||||
);
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "OwnerInfoOf is empty.");
|
||||
}
|
||||
|
||||
let mut deposit: v11::BalanceOf<T, OldCurrency> = Default::default();
|
||||
let mut items = 0u32;
|
||||
let mut storage_info = 0u32;
|
||||
CodeInfoOf::<T, OldCurrency>::iter().for_each(|(_k, v)| {
|
||||
deposit += v.deposit;
|
||||
items += 1;
|
||||
storage_info += v.encoded_size() as u32;
|
||||
});
|
||||
let mut storage_code = 0u32;
|
||||
PristineCode::<T>::iter().for_each(|(_k, v)| {
|
||||
storage_code += v.len() as u32;
|
||||
});
|
||||
let (_, old_deposit, storage_module) = state;
|
||||
// CodeInfoOf::max_encoded_len == OwnerInfoOf::max_encoded_len + 1
|
||||
// I.e. code info adds up 1 byte per record.
|
||||
let info_bytes_added = items;
|
||||
// We removed 1 PrefabWasmModule, and added 1 byte of determinism flag, per contract code.
|
||||
let storage_removed = storage_module.saturating_sub(info_bytes_added);
|
||||
// module+code+info - bytes
|
||||
let storage_was = storage_module
|
||||
.saturating_add(storage_code)
|
||||
.saturating_add(storage_info)
|
||||
.saturating_sub(info_bytes_added);
|
||||
// We removed 1 storage item (PrefabWasmMod) for every stored contract code (was stored 3
|
||||
// items per code).
|
||||
let items_removed = items;
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Storage freed, bytes: {} (of {}), items: {} (of {})",
|
||||
storage_removed,
|
||||
storage_was,
|
||||
items_removed,
|
||||
items_removed * 3,
|
||||
);
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Deposits returned, total: {:?} Balance (of {:?} Balance)",
|
||||
old_deposit.saturating_sub(deposit),
|
||||
old_deposit,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Add `delegate_dependencies` to `ContractInfo`.
|
||||
//! See <https://github.com/pezkuwichain/kurdistan-sdk/issues/49>.
|
||||
|
||||
use crate::{
|
||||
migration::{IsFinished, MigrationStep},
|
||||
weights::WeightInfo,
|
||||
AccountIdOf, BalanceOf, CodeHash, Config, Pallet, TrieId, Weight, LOG_TARGET,
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
use pezframe_support::{pezpallet_prelude::*, storage_alias, weights::WeightMeter, DefaultNoBound};
|
||||
use pezsp_runtime::BoundedBTreeMap;
|
||||
|
||||
mod v12 {
|
||||
use super::*;
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct ContractInfo<T: Config> {
|
||||
pub trie_id: TrieId,
|
||||
pub deposit_account: AccountIdOf<T>,
|
||||
pub code_hash: CodeHash<T>,
|
||||
pub storage_bytes: u32,
|
||||
pub storage_items: u32,
|
||||
pub storage_byte_deposit: BalanceOf<T>,
|
||||
pub storage_item_deposit: BalanceOf<T>,
|
||||
pub storage_base_deposit: BalanceOf<T>,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type ContractInfoOf<T: Config> = StorageMap<
|
||||
Pallet<T>,
|
||||
Twox64Concat,
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
ContractInfo<T>,
|
||||
>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn store_old_contract_info<T: Config>(account: T::AccountId, info: crate::ContractInfo<T>) {
|
||||
use pezsp_runtime::traits::{Hash, TrailingZeroInput};
|
||||
let entropy = (b"contract_depo_v1", account.clone()).using_encoded(T::Hashing::hash);
|
||||
let deposit_account = Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
|
||||
.expect("infinite length input; no invalid inputs for type; qed");
|
||||
let info = v12::ContractInfo {
|
||||
trie_id: info.trie_id.clone(),
|
||||
deposit_account,
|
||||
code_hash: info.code_hash,
|
||||
storage_bytes: Default::default(),
|
||||
storage_items: Default::default(),
|
||||
storage_byte_deposit: Default::default(),
|
||||
storage_item_deposit: Default::default(),
|
||||
storage_base_deposit: Default::default(),
|
||||
};
|
||||
v12::ContractInfoOf::<T>::insert(account, info);
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type ContractInfoOf<T: Config> =
|
||||
StorageMap<Pallet<T>, Twox64Concat, <T as pezframe_system::Config>::AccountId, ContractInfo<T>>;
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct ContractInfo<T: Config> {
|
||||
trie_id: TrieId,
|
||||
deposit_account: AccountIdOf<T>,
|
||||
code_hash: CodeHash<T>,
|
||||
storage_bytes: u32,
|
||||
storage_items: u32,
|
||||
storage_byte_deposit: BalanceOf<T>,
|
||||
storage_item_deposit: BalanceOf<T>,
|
||||
storage_base_deposit: BalanceOf<T>,
|
||||
delegate_dependencies: BoundedBTreeMap<CodeHash<T>, BalanceOf<T>, T::MaxDelegateDependencies>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
|
||||
pub struct Migration<T: Config> {
|
||||
last_account: Option<T::AccountId>,
|
||||
}
|
||||
|
||||
impl<T: Config> MigrationStep for Migration<T> {
|
||||
const VERSION: u16 = 13;
|
||||
|
||||
fn max_step_weight() -> Weight {
|
||||
T::WeightInfo::v13_migration_step()
|
||||
}
|
||||
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
let mut iter = if let Some(last_account) = self.last_account.take() {
|
||||
v12::ContractInfoOf::<T>::iter_from(v12::ContractInfoOf::<T>::hashed_key_for(
|
||||
last_account,
|
||||
))
|
||||
} else {
|
||||
v12::ContractInfoOf::<T>::iter()
|
||||
};
|
||||
|
||||
if let Some((key, old)) = iter.next() {
|
||||
log::debug!(target: LOG_TARGET, "Migrating contract {:?}", key);
|
||||
let info = ContractInfo {
|
||||
trie_id: old.trie_id,
|
||||
deposit_account: old.deposit_account,
|
||||
code_hash: old.code_hash,
|
||||
storage_bytes: old.storage_bytes,
|
||||
storage_items: old.storage_items,
|
||||
storage_byte_deposit: old.storage_byte_deposit,
|
||||
storage_item_deposit: old.storage_item_deposit,
|
||||
storage_base_deposit: old.storage_base_deposit,
|
||||
delegate_dependencies: Default::default(),
|
||||
};
|
||||
ContractInfoOf::<T>::insert(key.clone(), info);
|
||||
self.last_account = Some(key);
|
||||
meter.consume(T::WeightInfo::v13_migration_step());
|
||||
IsFinished::No
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "No more contracts to migrate");
|
||||
meter.consume(T::WeightInfo::v13_migration_step());
|
||||
IsFinished::Yes
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Update the code owner balance, make the code upload deposit balance to be held instead of
|
||||
//! reserved. Since [`Currency`](pezframe_support::traits::Currency) has been
|
||||
//! [deprecated](https://github.com/pezkuwichain/kurdistan-sdk/issues/40), we need the deposits to be
|
||||
//! handled by the [`pezframe_support::traits::fungible`] traits.
|
||||
|
||||
use crate::{
|
||||
exec::AccountIdOf,
|
||||
migration::{IsFinished, MigrationStep},
|
||||
weights::WeightInfo,
|
||||
BalanceOf, CodeHash, Config, Determinism, HoldReason, Pallet, Weight, LOG_TARGET,
|
||||
};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use alloc::collections::btree_map::BTreeMap;
|
||||
use codec::{Decode, Encode};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use environmental::Vec;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezframe_support::traits::fungible::{Inspect, InspectHold};
|
||||
use pezframe_support::{
|
||||
pezpallet_prelude::*,
|
||||
storage_alias,
|
||||
traits::{fungible::MutateHold, ReservableCurrency},
|
||||
weights::WeightMeter,
|
||||
DefaultNoBound,
|
||||
};
|
||||
use pezsp_core::hexdisplay::HexDisplay;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
use pezsp_runtime::{traits::Zero, Saturating};
|
||||
|
||||
mod v13 {
|
||||
use super::*;
|
||||
|
||||
pub type BalanceOf<T, OldCurrency> = <OldCurrency as pezframe_support::traits::Currency<
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
>>::Balance;
|
||||
|
||||
#[derive(Encode, Decode, scale_info::TypeInfo, MaxEncodedLen)]
|
||||
#[codec(mel_bound())]
|
||||
#[scale_info(skip_type_params(T, OldCurrency))]
|
||||
pub struct CodeInfo<T, OldCurrency>
|
||||
where
|
||||
T: Config,
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
{
|
||||
pub owner: AccountIdOf<T>,
|
||||
#[codec(compact)]
|
||||
pub deposit: v13::BalanceOf<T, OldCurrency>,
|
||||
#[codec(compact)]
|
||||
pub refcount: u64,
|
||||
pub determinism: Determinism,
|
||||
pub code_len: u32,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type CodeInfoOf<T: Config, OldCurrency> =
|
||||
StorageMap<Pallet<T>, Identity, CodeHash<T>, CodeInfo<T, OldCurrency>>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn store_dummy_code<T: Config, OldCurrency>(account: T::AccountId)
|
||||
where
|
||||
T: Config,
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId> + 'static,
|
||||
{
|
||||
use alloc::vec;
|
||||
use pezsp_runtime::traits::Hash;
|
||||
|
||||
let len = T::MaxCodeLen::get();
|
||||
let code = vec![42u8; len as usize];
|
||||
let hash = T::Hashing::hash(&code);
|
||||
|
||||
let info = v13::CodeInfo {
|
||||
owner: account,
|
||||
deposit: 10_000u32.into(),
|
||||
refcount: u64::MAX,
|
||||
determinism: Determinism::Enforced,
|
||||
code_len: len,
|
||||
};
|
||||
v13::CodeInfoOf::<T, OldCurrency>::insert(hash, info);
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
#[derive(Encode, Decode)]
|
||||
/// Accounts for the balance allocation of a code owner.
|
||||
struct BalanceAllocation<T, OldCurrency>
|
||||
where
|
||||
T: Config,
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
{
|
||||
/// Total reserved balance as code upload deposit for the owner.
|
||||
reserved: v13::BalanceOf<T, OldCurrency>,
|
||||
/// Total balance of the owner.
|
||||
total: v13::BalanceOf<T, OldCurrency>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
|
||||
pub struct Migration<T, OldCurrency>
|
||||
where
|
||||
T: Config,
|
||||
OldCurrency: ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
{
|
||||
last_code_hash: Option<CodeHash<T>>,
|
||||
_phantom: PhantomData<(T, OldCurrency)>,
|
||||
}
|
||||
|
||||
impl<T, OldCurrency> MigrationStep for Migration<T, OldCurrency>
|
||||
where
|
||||
T: Config,
|
||||
OldCurrency: 'static + ReservableCurrency<<T as pezframe_system::Config>::AccountId>,
|
||||
BalanceOf<T>: From<OldCurrency::Balance>,
|
||||
{
|
||||
const VERSION: u16 = 14;
|
||||
|
||||
fn max_step_weight() -> Weight {
|
||||
T::WeightInfo::v14_migration_step()
|
||||
}
|
||||
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
let mut iter = if let Some(last_hash) = self.last_code_hash.take() {
|
||||
v13::CodeInfoOf::<T, OldCurrency>::iter_from(
|
||||
v13::CodeInfoOf::<T, OldCurrency>::hashed_key_for(last_hash),
|
||||
)
|
||||
} else {
|
||||
v13::CodeInfoOf::<T, OldCurrency>::iter()
|
||||
};
|
||||
|
||||
if let Some((hash, code_info)) = iter.next() {
|
||||
log::debug!(target: LOG_TARGET, "Migrating code upload deposit for 0x{:?}", HexDisplay::from(&code_info.owner.encode()));
|
||||
|
||||
let remaining = OldCurrency::unreserve(&code_info.owner, code_info.deposit);
|
||||
|
||||
if remaining > Zero::zero() {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"Code owner's account 0x{:?} for code {:?} has some non-unreservable deposit {:?} from a total of {:?} that will remain in reserved.",
|
||||
HexDisplay::from(&code_info.owner.encode()),
|
||||
hash,
|
||||
remaining,
|
||||
code_info.deposit
|
||||
);
|
||||
}
|
||||
|
||||
let unreserved = code_info.deposit.saturating_sub(remaining);
|
||||
let amount = BalanceOf::<T>::from(unreserved);
|
||||
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Holding {:?} on the code owner's account 0x{:?} for code {:?}.",
|
||||
amount,
|
||||
HexDisplay::from(&code_info.owner.encode()),
|
||||
hash,
|
||||
);
|
||||
|
||||
T::Currency::hold(
|
||||
&HoldReason::CodeUploadDepositReserve.into(),
|
||||
&code_info.owner,
|
||||
amount,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to hold {:?} from the code owner's account 0x{:?} for code {:?}, reason: {:?}.",
|
||||
amount,
|
||||
HexDisplay::from(&code_info.owner.encode()),
|
||||
hash,
|
||||
err
|
||||
);
|
||||
});
|
||||
|
||||
self.last_code_hash = Some(hash);
|
||||
meter.consume(T::WeightInfo::v14_migration_step());
|
||||
IsFinished::No
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "No more code upload deposit to migrate");
|
||||
meter.consume(T::WeightInfo::v14_migration_step());
|
||||
IsFinished::Yes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade_step() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
let info: Vec<_> = v13::CodeInfoOf::<T, OldCurrency>::iter().collect();
|
||||
|
||||
let mut owner_balance_allocation =
|
||||
BTreeMap::<AccountIdOf<T>, BalanceAllocation<T, OldCurrency>>::new();
|
||||
|
||||
// Calculates the balance allocation by accumulating the code upload deposits of all codes
|
||||
// owned by an owner.
|
||||
for (_, code_info) in info {
|
||||
owner_balance_allocation
|
||||
.entry(code_info.owner.clone())
|
||||
.and_modify(|alloc| {
|
||||
alloc.reserved = alloc.reserved.saturating_add(code_info.deposit);
|
||||
})
|
||||
.or_insert(BalanceAllocation {
|
||||
reserved: code_info.deposit,
|
||||
total: OldCurrency::total_balance(&code_info.owner),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(owner_balance_allocation.encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let owner_balance_allocation =
|
||||
<BTreeMap<AccountIdOf<T>, BalanceAllocation<T, OldCurrency>> as Decode>::decode(
|
||||
&mut &state[..],
|
||||
)
|
||||
.expect("pre_upgrade_step provides a valid state; qed");
|
||||
|
||||
let mut total_held: BalanceOf<T> = Zero::zero();
|
||||
let count = owner_balance_allocation.len();
|
||||
for (owner, old_balance_allocation) in owner_balance_allocation {
|
||||
let held =
|
||||
T::Currency::balance_on_hold(&HoldReason::CodeUploadDepositReserve.into(), &owner);
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Validating code upload deposit for owner 0x{:?}, reserved: {:?}, held: {:?}",
|
||||
HexDisplay::from(&owner.encode()),
|
||||
old_balance_allocation.reserved,
|
||||
held
|
||||
);
|
||||
ensure!(held == old_balance_allocation.reserved.into(), "Held amount mismatch");
|
||||
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Validating total balance for owner 0x{:?}, new: {:?}, old: {:?}",
|
||||
HexDisplay::from(&owner.encode()),
|
||||
T::Currency::total_balance(&owner),
|
||||
old_balance_allocation.total
|
||||
);
|
||||
ensure!(
|
||||
T::Currency::total_balance(&owner) ==
|
||||
BalanceOf::<T>::decode(&mut &old_balance_allocation.total.encode()[..])
|
||||
.unwrap(),
|
||||
"Balance mismatch "
|
||||
);
|
||||
total_held += held;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Code owners processed: {:?}.",
|
||||
count
|
||||
);
|
||||
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Total held amount for code upload deposit: {:?}",
|
||||
total_held
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Move contracts' _reserved_ balance from the `deposit_account` to be _held_ in the contract's
|
||||
//! account instead. Since [`Currency`](pezframe_support::traits::Currency) has been
|
||||
//! [deprecated](https://github.com/pezkuwichain/kurdistan-sdk/issues/40), we need the deposits to be
|
||||
//! handled by the [`pezframe_support::traits::fungible`] traits instead. For this transfer the
|
||||
//! balance from the deposit account to the contract's account and hold it in there.
|
||||
//! Then the deposit account is not needed anymore and we can get rid of it.
|
||||
|
||||
use crate::{
|
||||
migration::{IsFinished, MigrationStep},
|
||||
weights::WeightInfo,
|
||||
AccountIdOf, BalanceOf, CodeHash, Config, HoldReason, Pallet, TrieId, Weight, LOG_TARGET,
|
||||
};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezframe_support::traits::fungible::InspectHold;
|
||||
use pezframe_support::{
|
||||
pezpallet_prelude::*,
|
||||
storage_alias,
|
||||
traits::{
|
||||
fungible::{Mutate, MutateHold},
|
||||
tokens::{fungible::Inspect, Fortitude, Preservation},
|
||||
},
|
||||
weights::WeightMeter,
|
||||
BoundedBTreeMap, DefaultNoBound,
|
||||
};
|
||||
use pezframe_system::Pallet as System;
|
||||
use pezsp_core::hexdisplay::HexDisplay;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
use pezsp_runtime::{traits::Zero, Saturating};
|
||||
|
||||
mod v14 {
|
||||
use super::*;
|
||||
|
||||
#[derive(
|
||||
Encode, Decode, CloneNoBound, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct ContractInfo<T: Config> {
|
||||
pub trie_id: TrieId,
|
||||
pub deposit_account: AccountIdOf<T>,
|
||||
pub code_hash: CodeHash<T>,
|
||||
pub storage_bytes: u32,
|
||||
pub storage_items: u32,
|
||||
pub storage_byte_deposit: BalanceOf<T>,
|
||||
pub storage_item_deposit: BalanceOf<T>,
|
||||
pub storage_base_deposit: BalanceOf<T>,
|
||||
pub delegate_dependencies:
|
||||
BoundedBTreeMap<CodeHash<T>, BalanceOf<T>, T::MaxDelegateDependencies>,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type ContractInfoOf<T: Config> = StorageMap<
|
||||
Pallet<T>,
|
||||
Twox64Concat,
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
ContractInfo<T>,
|
||||
>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn store_old_contract_info<T: Config>(account: T::AccountId, info: crate::ContractInfo<T>) {
|
||||
use pezsp_runtime::traits::{Hash, TrailingZeroInput};
|
||||
let entropy = (b"contract_depo_v1", account.clone()).using_encoded(T::Hashing::hash);
|
||||
let deposit_account = Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
|
||||
.expect("infinite length input; no invalid inputs for type; qed");
|
||||
let info = v14::ContractInfo {
|
||||
trie_id: info.trie_id.clone(),
|
||||
deposit_account,
|
||||
code_hash: info.code_hash,
|
||||
storage_bytes: Default::default(),
|
||||
storage_items: Default::default(),
|
||||
storage_byte_deposit: info.storage_byte_deposit,
|
||||
storage_item_deposit: Default::default(),
|
||||
storage_base_deposit: info.storage_base_deposit(),
|
||||
delegate_dependencies: info.delegate_dependencies().clone(),
|
||||
};
|
||||
v14::ContractInfoOf::<T>::insert(account, info);
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
struct ContractInfo<T: Config> {
|
||||
pub trie_id: TrieId,
|
||||
pub code_hash: CodeHash<T>,
|
||||
pub storage_bytes: u32,
|
||||
pub storage_items: u32,
|
||||
pub storage_byte_deposit: BalanceOf<T>,
|
||||
pub storage_item_deposit: BalanceOf<T>,
|
||||
pub storage_base_deposit: BalanceOf<T>,
|
||||
pub delegate_dependencies:
|
||||
BoundedBTreeMap<CodeHash<T>, BalanceOf<T>, T::MaxDelegateDependencies>,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
type ContractInfoOf<T: Config> =
|
||||
StorageMap<Pallet<T>, Twox64Concat, <T as pezframe_system::Config>::AccountId, ContractInfo<T>>;
|
||||
|
||||
#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
|
||||
pub struct Migration<T: Config> {
|
||||
last_account: Option<T::AccountId>,
|
||||
}
|
||||
|
||||
impl<T: Config> MigrationStep for Migration<T> {
|
||||
const VERSION: u16 = 15;
|
||||
|
||||
fn max_step_weight() -> Weight {
|
||||
T::WeightInfo::v15_migration_step()
|
||||
}
|
||||
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
let mut iter = if let Some(last_account) = self.last_account.take() {
|
||||
v14::ContractInfoOf::<T>::iter_from(v14::ContractInfoOf::<T>::hashed_key_for(
|
||||
last_account,
|
||||
))
|
||||
} else {
|
||||
v14::ContractInfoOf::<T>::iter()
|
||||
};
|
||||
|
||||
if let Some((account, old_contract)) = iter.next() {
|
||||
let deposit_account = &old_contract.deposit_account;
|
||||
System::<T>::dec_consumers(deposit_account);
|
||||
|
||||
// Get the deposit balance to transfer.
|
||||
let total_deposit_balance = T::Currency::total_balance(deposit_account);
|
||||
let reducible_deposit_balance = T::Currency::reducible_balance(
|
||||
deposit_account,
|
||||
Preservation::Expendable,
|
||||
Fortitude::Force,
|
||||
);
|
||||
|
||||
if total_deposit_balance > reducible_deposit_balance {
|
||||
// This should never happen, as by design all balance in the deposit account is
|
||||
// storage deposit and therefore reducible after decrementing the consumer
|
||||
// reference.
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"Deposit account 0x{:?} for contract 0x{:?} has some non-reducible balance {:?} from a total of {:?} that will remain in there.",
|
||||
HexDisplay::from(&deposit_account.encode()),
|
||||
HexDisplay::from(&account.encode()),
|
||||
total_deposit_balance.saturating_sub(reducible_deposit_balance),
|
||||
total_deposit_balance
|
||||
);
|
||||
}
|
||||
|
||||
// Move balance reserved from the deposit account back to the contract account.
|
||||
// Let the deposit account die.
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Transferring {:?} from the deposit account 0x{:?} to the contract 0x{:?}.",
|
||||
reducible_deposit_balance,
|
||||
HexDisplay::from(&deposit_account.encode()),
|
||||
HexDisplay::from(&account.encode())
|
||||
);
|
||||
let transferred_deposit_balance = T::Currency::transfer(
|
||||
deposit_account,
|
||||
&account,
|
||||
reducible_deposit_balance,
|
||||
Preservation::Expendable,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to transfer {:?} from the deposit account 0x{:?} to the contract 0x{:?}, reason: {:?}.",
|
||||
reducible_deposit_balance,
|
||||
HexDisplay::from(&deposit_account.encode()),
|
||||
HexDisplay::from(&account.encode()),
|
||||
err
|
||||
);
|
||||
Zero::zero()
|
||||
});
|
||||
|
||||
// Hold the reserved balance.
|
||||
if transferred_deposit_balance == Zero::zero() {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"No balance to hold as storage deposit on the contract 0x{:?}.",
|
||||
HexDisplay::from(&account.encode())
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Holding {:?} as storage deposit on the contract 0x{:?}.",
|
||||
transferred_deposit_balance,
|
||||
HexDisplay::from(&account.encode())
|
||||
);
|
||||
|
||||
T::Currency::hold(
|
||||
&HoldReason::StorageDepositReserve.into(),
|
||||
&account,
|
||||
transferred_deposit_balance,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to hold {:?} as storage deposit on the contract 0x{:?}, reason: {:?}.",
|
||||
transferred_deposit_balance,
|
||||
HexDisplay::from(&account.encode()),
|
||||
err
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
log::debug!(target: LOG_TARGET, "===");
|
||||
let info = ContractInfo {
|
||||
trie_id: old_contract.trie_id,
|
||||
code_hash: old_contract.code_hash,
|
||||
storage_bytes: old_contract.storage_bytes,
|
||||
storage_items: old_contract.storage_items,
|
||||
storage_byte_deposit: old_contract.storage_byte_deposit,
|
||||
storage_item_deposit: old_contract.storage_item_deposit,
|
||||
storage_base_deposit: old_contract.storage_base_deposit,
|
||||
delegate_dependencies: old_contract.delegate_dependencies,
|
||||
};
|
||||
ContractInfoOf::<T>::insert(account.clone(), info);
|
||||
|
||||
// Store last key for next migration step
|
||||
self.last_account = Some(account);
|
||||
|
||||
meter.consume(T::WeightInfo::v15_migration_step());
|
||||
IsFinished::No
|
||||
} else {
|
||||
log::info!(target: LOG_TARGET, "Done Migrating Storage Deposits.");
|
||||
meter.consume(T::WeightInfo::v15_migration_step());
|
||||
IsFinished::Yes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade_step() -> Result<Vec<u8>, TryRuntimeError> {
|
||||
let sample: Vec<_> = v14::ContractInfoOf::<T>::iter().take(100).collect();
|
||||
|
||||
log::debug!(target: LOG_TARGET, "Taking sample of {} contracts", sample.len());
|
||||
|
||||
let state: Vec<(T::AccountId, v14::ContractInfo<T>, BalanceOf<T>, BalanceOf<T>)> = sample
|
||||
.iter()
|
||||
.map(|(account, contract)| {
|
||||
(
|
||||
account.clone(),
|
||||
contract.clone(),
|
||||
T::Currency::total_balance(&account),
|
||||
T::Currency::total_balance(&contract.deposit_account),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(state.encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let sample =
|
||||
<Vec<(T::AccountId, v14::ContractInfo<T>, BalanceOf<T>, BalanceOf<T>)> as Decode>::decode(
|
||||
&mut &state[..],
|
||||
)
|
||||
.expect("pre_upgrade_step provides a valid state; qed");
|
||||
|
||||
log::debug!(target: LOG_TARGET, "Validating sample of {} contracts", sample.len());
|
||||
for (account, old_contract, old_account_balance, old_deposit_balance) in sample {
|
||||
log::debug!(target: LOG_TARGET, "===");
|
||||
log::debug!(target: LOG_TARGET, "Account: 0x{} ", HexDisplay::from(&account.encode()));
|
||||
|
||||
let on_hold =
|
||||
T::Currency::balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account);
|
||||
let account_balance = T::Currency::total_balance(&account);
|
||||
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Validating balances match. Old deposit account's balance: {:?}. Contract's on hold: {:?}. Old contract's total balance: {:?}, Contract's total balance: {:?}.",
|
||||
old_deposit_balance,
|
||||
on_hold,
|
||||
old_account_balance,
|
||||
account_balance
|
||||
);
|
||||
ensure!(
|
||||
old_account_balance.saturating_add(old_deposit_balance) == account_balance,
|
||||
"total balance mismatch"
|
||||
);
|
||||
ensure!(old_deposit_balance == on_hold, "deposit mismatch");
|
||||
ensure!(
|
||||
!System::<T>::account_exists(&old_contract.deposit_account),
|
||||
"deposit account still exists"
|
||||
);
|
||||
|
||||
let migration_contract_info = ContractInfoOf::<T>::try_get(&account).unwrap();
|
||||
let crate_contract_info = crate::ContractInfoOf::<T>::try_get(&account).unwrap();
|
||||
ensure!(
|
||||
migration_contract_info.trie_id == crate_contract_info.trie_id,
|
||||
"trie_id mismatch"
|
||||
);
|
||||
ensure!(
|
||||
migration_contract_info.code_hash == crate_contract_info.code_hash,
|
||||
"code_hash mismatch"
|
||||
);
|
||||
ensure!(
|
||||
migration_contract_info.storage_byte_deposit ==
|
||||
crate_contract_info.storage_byte_deposit,
|
||||
"storage_byte_deposit mismatch"
|
||||
);
|
||||
ensure!(
|
||||
migration_contract_info.storage_base_deposit ==
|
||||
crate_contract_info.storage_base_deposit(),
|
||||
"storage_base_deposit mismatch"
|
||||
);
|
||||
ensure!(
|
||||
&migration_contract_info.delegate_dependencies ==
|
||||
crate_contract_info.delegate_dependencies(),
|
||||
"delegate_dependencies mismatch"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Remove ED from storage base deposit.
|
||||
//! See <https://github.com/pezkuwichain/kurdistan-sdk/issues/116>.
|
||||
|
||||
use crate::{
|
||||
migration::{IsFinished, MigrationStep},
|
||||
weights::WeightInfo,
|
||||
BalanceOf, CodeHash, Config, Pallet, TrieId, Weight, WeightMeter, LOG_TARGET,
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
use pezframe_support::{pezpallet_prelude::*, storage_alias, DefaultNoBound};
|
||||
use pezsp_runtime::{BoundedBTreeMap, Saturating};
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn store_old_contract_info<T: Config>(
|
||||
account: T::AccountId,
|
||||
info: &crate::ContractInfo<T>,
|
||||
) -> BalanceOf<T> {
|
||||
let storage_base_deposit = Pallet::<T>::min_balance() + 1u32.into();
|
||||
ContractInfoOf::<T>::insert(
|
||||
account,
|
||||
ContractInfo {
|
||||
trie_id: info.trie_id.clone(),
|
||||
code_hash: info.code_hash,
|
||||
storage_bytes: Default::default(),
|
||||
storage_items: Default::default(),
|
||||
storage_byte_deposit: Default::default(),
|
||||
storage_item_deposit: Default::default(),
|
||||
storage_base_deposit,
|
||||
delegate_dependencies: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
storage_base_deposit
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type ContractInfoOf<T: Config> =
|
||||
StorageMap<Pallet<T>, Twox64Concat, <T as pezframe_system::Config>::AccountId, ContractInfo<T>>;
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct ContractInfo<T: Config> {
|
||||
trie_id: TrieId,
|
||||
code_hash: CodeHash<T>,
|
||||
storage_bytes: u32,
|
||||
storage_items: u32,
|
||||
storage_byte_deposit: BalanceOf<T>,
|
||||
storage_item_deposit: BalanceOf<T>,
|
||||
pub storage_base_deposit: BalanceOf<T>,
|
||||
delegate_dependencies: BoundedBTreeMap<CodeHash<T>, BalanceOf<T>, T::MaxDelegateDependencies>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
|
||||
pub struct Migration<T: Config> {
|
||||
last_account: Option<T::AccountId>,
|
||||
}
|
||||
|
||||
impl<T: Config> MigrationStep for Migration<T> {
|
||||
const VERSION: u16 = 16;
|
||||
|
||||
fn max_step_weight() -> Weight {
|
||||
T::WeightInfo::v16_migration_step()
|
||||
}
|
||||
|
||||
fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
|
||||
let mut iter = if let Some(last_account) = self.last_account.take() {
|
||||
ContractInfoOf::<T>::iter_keys_from(ContractInfoOf::<T>::hashed_key_for(last_account))
|
||||
} else {
|
||||
ContractInfoOf::<T>::iter_keys()
|
||||
};
|
||||
|
||||
if let Some(key) = iter.next() {
|
||||
log::debug!(target: LOG_TARGET, "Migrating contract {:?}", key);
|
||||
ContractInfoOf::<T>::mutate(key.clone(), |info| {
|
||||
let ed = Pallet::<T>::min_balance();
|
||||
let mut updated_info = info.take().expect("Item exists; qed");
|
||||
updated_info.storage_base_deposit.saturating_reduce(ed);
|
||||
*info = Some(updated_info);
|
||||
});
|
||||
self.last_account = Some(key);
|
||||
meter.consume(T::WeightInfo::v16_migration_step());
|
||||
IsFinished::No
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "No more contracts to migrate");
|
||||
meter.consume(T::WeightInfo::v16_migration_step());
|
||||
IsFinished::Yes
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! A crate that hosts a common definitions that are relevant for the pezpallet-contracts.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use pezframe_support::weights::Weight;
|
||||
use pezpallet_contracts_uapi::ReturnFlags;
|
||||
use scale_info::TypeInfo;
|
||||
use pezsp_runtime::{
|
||||
traits::{Saturating, Zero},
|
||||
DispatchError, RuntimeDebug,
|
||||
};
|
||||
|
||||
/// Result type of a `bare_call` or `bare_instantiate` call as well as `ContractsApi::call` and
|
||||
/// `ContractsApi::instantiate`.
|
||||
///
|
||||
/// It contains the execution result together with some auxiliary information.
|
||||
///
|
||||
/// #Note
|
||||
///
|
||||
/// It has been extended to include `events` at the end of the struct while not bumping the
|
||||
/// `ContractsApi` version. Therefore when SCALE decoding a `ContractResult` its trailing data
|
||||
/// should be ignored to avoid any potential compatibility issues.
|
||||
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub struct ContractResult<R, Balance, EventRecord> {
|
||||
/// How much weight was consumed during execution.
|
||||
pub gas_consumed: Weight,
|
||||
/// How much weight is required as gas limit in order to execute this call.
|
||||
///
|
||||
/// This value should be used to determine the weight limit for on-chain execution.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This can only different from [`Self::gas_consumed`] when weight pre charging
|
||||
/// is used. Currently, only `seal_call_runtime` makes use of pre charging.
|
||||
/// Additionally, any `seal_call` or `seal_instantiate` makes use of pre-charging
|
||||
/// when a non-zero `gas_limit` argument is supplied.
|
||||
pub gas_required: Weight,
|
||||
/// How much balance was paid by the origin into the contract's deposit account in order to
|
||||
/// pay for storage.
|
||||
///
|
||||
/// The storage deposit is never actually charged from the origin in case of [`Self::result`]
|
||||
/// is `Err`. This is because on error all storage changes are rolled back including the
|
||||
/// payment of the deposit.
|
||||
pub storage_deposit: StorageDeposit<Balance>,
|
||||
/// An optional debug message. This message is only filled when explicitly requested
|
||||
/// by the code that calls into the contract. Otherwise it is empty.
|
||||
///
|
||||
/// The contained bytes are valid UTF-8. This is not declared as `String` because
|
||||
/// this type is not allowed within the runtime.
|
||||
///
|
||||
/// Clients should not make any assumptions about the format of the buffer.
|
||||
/// They should just display it as-is. It is **not** only a collection of log lines
|
||||
/// provided by a contract but a formatted buffer with different sections.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// The debug message is never generated during on-chain execution. It is reserved for
|
||||
/// RPC calls.
|
||||
pub debug_message: Vec<u8>,
|
||||
/// The execution result of the wasm code.
|
||||
pub result: R,
|
||||
/// The events that were emitted during execution. It is an option as event collection is
|
||||
/// optional.
|
||||
pub events: Option<Vec<EventRecord>>,
|
||||
}
|
||||
|
||||
/// Result type of a `bare_call` call as well as `ContractsApi::call`.
|
||||
pub type ContractExecResult<Balance, EventRecord> =
|
||||
ContractResult<Result<ExecReturnValue, DispatchError>, Balance, EventRecord>;
|
||||
|
||||
/// Result type of a `bare_instantiate` call as well as `ContractsApi::instantiate`.
|
||||
pub type ContractInstantiateResult<AccountId, Balance, EventRecord> =
|
||||
ContractResult<Result<InstantiateReturnValue<AccountId>, DispatchError>, Balance, EventRecord>;
|
||||
|
||||
/// Result type of a `bare_code_upload` call.
|
||||
pub type CodeUploadResult<CodeHash, Balance> =
|
||||
Result<CodeUploadReturnValue<CodeHash, Balance>, DispatchError>;
|
||||
|
||||
/// Result type of a `get_storage` call.
|
||||
pub type GetStorageResult = Result<Option<Vec<u8>>, ContractAccessError>;
|
||||
|
||||
/// The possible errors that can happen querying the storage of a contract.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Encode, Decode, MaxEncodedLen, RuntimeDebug, TypeInfo)]
|
||||
pub enum ContractAccessError {
|
||||
/// The given address doesn't point to a contract.
|
||||
DoesntExist,
|
||||
/// Storage key cannot be decoded from the provided input data.
|
||||
KeyDecodingFailed,
|
||||
/// Storage is migrating. Try again later.
|
||||
MigrationInProgress,
|
||||
}
|
||||
|
||||
/// Output of a contract call or instantiation which ran to completion.
|
||||
#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub struct ExecReturnValue {
|
||||
/// Flags passed along by `seal_return`. Empty when `seal_return` was never called.
|
||||
pub flags: ReturnFlags,
|
||||
/// Buffer passed along by `seal_return`. Empty when `seal_return` was never called.
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ExecReturnValue {
|
||||
/// The contract did revert all storage changes.
|
||||
pub fn did_revert(&self) -> bool {
|
||||
self.flags.contains(ReturnFlags::REVERT)
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a successful contract instantiation.
|
||||
#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub struct InstantiateReturnValue<AccountId> {
|
||||
/// The output of the called constructor.
|
||||
pub result: ExecReturnValue,
|
||||
/// The account id of the new contract.
|
||||
pub account_id: AccountId,
|
||||
}
|
||||
|
||||
/// The result of successfully uploading a contract.
|
||||
#[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, RuntimeDebug, TypeInfo)]
|
||||
pub struct CodeUploadReturnValue<CodeHash, Balance> {
|
||||
/// The key under which the new code is stored.
|
||||
pub code_hash: CodeHash,
|
||||
/// The deposit that was reserved at the caller. Is zero when the code already existed.
|
||||
pub deposit: Balance,
|
||||
}
|
||||
|
||||
/// Reference to an existing code hash or a new wasm module.
|
||||
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub enum Code<Hash> {
|
||||
/// A wasm module as raw bytes.
|
||||
Upload(Vec<u8>),
|
||||
/// The code hash of an on-chain wasm blob.
|
||||
Existing(Hash),
|
||||
}
|
||||
|
||||
/// The amount of balance that was either charged or refunded in order to pay for storage.
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, RuntimeDebug, TypeInfo,
|
||||
)]
|
||||
pub enum StorageDeposit<Balance> {
|
||||
/// The transaction reduced storage consumption.
|
||||
///
|
||||
/// This means that the specified amount of balance was transferred from the involved
|
||||
/// deposit accounts to the origin.
|
||||
Refund(Balance),
|
||||
/// The transaction increased storage consumption.
|
||||
///
|
||||
/// This means that the specified amount of balance was transferred from the origin
|
||||
/// to the involved deposit accounts.
|
||||
Charge(Balance),
|
||||
}
|
||||
|
||||
impl<Balance: Zero> Default for StorageDeposit<Balance> {
|
||||
fn default() -> Self {
|
||||
Self::Charge(Zero::zero())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Balance: Zero + Copy> StorageDeposit<Balance> {
|
||||
/// Returns how much balance is charged or `0` in case of a refund.
|
||||
pub fn charge_or_zero(&self) -> Balance {
|
||||
match self {
|
||||
Self::Charge(amount) => *amount,
|
||||
Self::Refund(_) => Zero::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_zero(&self) -> bool {
|
||||
match self {
|
||||
Self::Charge(amount) => amount.is_zero(),
|
||||
Self::Refund(amount) => amount.is_zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Balance> StorageDeposit<Balance>
|
||||
where
|
||||
Balance: Saturating + Ord + Copy,
|
||||
{
|
||||
/// This is essentially a saturating signed add.
|
||||
pub fn saturating_add(&self, rhs: &Self) -> Self {
|
||||
use StorageDeposit::*;
|
||||
match (self, rhs) {
|
||||
(Charge(lhs), Charge(rhs)) => Charge(lhs.saturating_add(*rhs)),
|
||||
(Refund(lhs), Refund(rhs)) => Refund(lhs.saturating_add(*rhs)),
|
||||
(Charge(lhs), Refund(rhs)) =>
|
||||
if lhs >= rhs {
|
||||
Charge(lhs.saturating_sub(*rhs))
|
||||
} else {
|
||||
Refund(rhs.saturating_sub(*lhs))
|
||||
},
|
||||
(Refund(lhs), Charge(rhs)) =>
|
||||
if lhs > rhs {
|
||||
Refund(lhs.saturating_sub(*rhs))
|
||||
} else {
|
||||
Charge(rhs.saturating_sub(*lhs))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// This is essentially a saturating signed sub.
|
||||
pub fn saturating_sub(&self, rhs: &Self) -> Self {
|
||||
use StorageDeposit::*;
|
||||
match (self, rhs) {
|
||||
(Charge(lhs), Refund(rhs)) => Charge(lhs.saturating_add(*rhs)),
|
||||
(Refund(lhs), Charge(rhs)) => Refund(lhs.saturating_add(*rhs)),
|
||||
(Charge(lhs), Charge(rhs)) =>
|
||||
if lhs >= rhs {
|
||||
Charge(lhs.saturating_sub(*rhs))
|
||||
} else {
|
||||
Refund(rhs.saturating_sub(*lhs))
|
||||
},
|
||||
(Refund(lhs), Refund(rhs)) =>
|
||||
if lhs > rhs {
|
||||
Refund(lhs.saturating_sub(*rhs))
|
||||
} else {
|
||||
Charge(rhs.saturating_sub(*lhs))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// If the amount of deposit (this type) is constrained by a `limit` this calculates how
|
||||
/// much balance (if any) is still available from this limit.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// In case of a refund the return value can be larger than `limit`.
|
||||
pub fn available(&self, limit: &Balance) -> Balance {
|
||||
use StorageDeposit::*;
|
||||
match self {
|
||||
Charge(amount) => limit.saturating_sub(*amount),
|
||||
Refund(amount) => limit.saturating_add(*amount),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! This module contains the cost schedule and supporting code that constructs a
|
||||
//! sane default schedule from a `WeightInfo` implementation.
|
||||
|
||||
use crate::{weights::WeightInfo, Config};
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use core::marker::PhantomData;
|
||||
use pezframe_support::DefaultNoBound;
|
||||
use scale_info::TypeInfo;
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Definition of the cost schedule and other parameterizations for the wasm vm.
|
||||
///
|
||||
/// Its [`Default`] implementation is the designated way to initialize this type. It uses
|
||||
/// the benchmarked information supplied by [`Config::WeightInfo`]. All of its fields are
|
||||
/// public and can therefore be modified. For example in order to change some of the limits
|
||||
/// and set a custom instruction weight version the following code could be used:
|
||||
/// ```rust
|
||||
/// use pezpallet_contracts::{Schedule, Limits, InstructionWeights, Config};
|
||||
///
|
||||
/// fn create_schedule<T: Config>() -> Schedule<T> {
|
||||
/// Schedule {
|
||||
/// limits: Limits {
|
||||
/// memory_pages: 16,
|
||||
/// .. Default::default()
|
||||
/// },
|
||||
/// instruction_weights: InstructionWeights {
|
||||
/// .. Default::default()
|
||||
/// },
|
||||
/// .. Default::default()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "std", serde(bound(serialize = "", deserialize = "")))]
|
||||
#[cfg_attr(feature = "runtime-benchmarks", derive(pezframe_support::DebugNoBound))]
|
||||
#[derive(Clone, Encode, Decode, PartialEq, Eq, DefaultNoBound, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Schedule<T: Config> {
|
||||
/// Describes the upper limits on various metrics.
|
||||
pub limits: Limits,
|
||||
|
||||
/// The weights for individual wasm instructions.
|
||||
pub instruction_weights: InstructionWeights<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Schedule<T> {
|
||||
/// Returns the reference time per engine fuel.
|
||||
pub fn ref_time_by_fuel(&self) -> u64 {
|
||||
self.instruction_weights.base as u64
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the upper limits on various metrics.
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "runtime-benchmarks", derive(Debug))]
|
||||
#[derive(Clone, Encode, Decode, PartialEq, Eq, TypeInfo)]
|
||||
pub struct Limits {
|
||||
/// The maximum number of topics supported by an event.
|
||||
pub event_topics: u32,
|
||||
|
||||
/// Maximum number of memory pages allowed for a contract.
|
||||
pub memory_pages: u32,
|
||||
|
||||
/// The maximum length of a subject in bytes used for PRNG generation.
|
||||
pub subject_len: u32,
|
||||
|
||||
/// The maximum size of a storage value and event payload in bytes.
|
||||
pub payload_len: u32,
|
||||
|
||||
/// The maximum node runtime memory. This is for integrity checks only and does not affect the
|
||||
/// real setting.
|
||||
pub runtime_memory: u32,
|
||||
|
||||
/// The maximum validator node runtime memory. This is for integrity checks only and does not
|
||||
/// affect the real setting.
|
||||
pub validator_runtime_memory: u32,
|
||||
|
||||
/// The additional ref_time added to the `deposit_event` host function call per event data
|
||||
/// byte.
|
||||
pub event_ref_time: u64,
|
||||
}
|
||||
|
||||
impl Limits {
|
||||
/// The maximum memory size in bytes that a contract can occupy.
|
||||
pub fn max_memory_size(&self) -> u32 {
|
||||
self.memory_pages * 64 * 1024
|
||||
}
|
||||
}
|
||||
|
||||
/// Gas metering of Wasm executed instructions is being done on the engine side.
|
||||
/// This struct holds a reference value used to gas units scaling between host and engine.
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "runtime-benchmarks", derive(pezframe_support::DebugNoBound))]
|
||||
#[derive(Clone, Encode, Decode, PartialEq, Eq, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct InstructionWeights<T: Config> {
|
||||
/// Base instruction `ref_time` Weight.
|
||||
/// Should match to wasmi's `1` fuel (see <https://github.com/wasmi-labs/wasmi/issues/701>).
|
||||
pub base: u32,
|
||||
/// The type parameter is used in the default implementation.
|
||||
#[codec(skip)]
|
||||
pub _phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
event_topics: 4,
|
||||
memory_pages: 16,
|
||||
subject_len: 32,
|
||||
payload_len: 16 * 1024,
|
||||
runtime_memory: 1024 * 1024 * 128,
|
||||
validator_runtime_memory: 1024 * 1024 * 512,
|
||||
event_ref_time: 60_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Default for InstructionWeights<T> {
|
||||
/// We execute 6 different instructions therefore we have to divide the actual
|
||||
/// computed gas costs by 6 to have a rough estimate as to how expensive each
|
||||
/// single executed instruction is going to be.
|
||||
fn default() -> Self {
|
||||
let instr_cost = T::WeightInfo::instr_i64_load_store(1)
|
||||
.saturating_sub(T::WeightInfo::instr_i64_load_store(0))
|
||||
.ref_time() as u32;
|
||||
let base = instr_cost / 6;
|
||||
Self { base, _phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! This module contains routines for accessing and altering a contract related state.
|
||||
|
||||
pub mod meter;
|
||||
|
||||
use crate::{
|
||||
exec::{AccountIdOf, Key},
|
||||
weights::WeightInfo,
|
||||
BalanceOf, CodeHash, CodeInfo, Config, ContractInfoOf, DeletionQueue, DeletionQueueCounter,
|
||||
Error, TrieId, SENTINEL,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use core::marker::PhantomData;
|
||||
use pezframe_support::{
|
||||
storage::child::{self, ChildInfo},
|
||||
weights::{Weight, WeightMeter},
|
||||
CloneNoBound, DefaultNoBound,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use pezsp_core::Get;
|
||||
use pezsp_io::KillStorageResult;
|
||||
use pezsp_runtime::{
|
||||
traits::{Hash, Saturating, Zero},
|
||||
BoundedBTreeMap, DispatchError, DispatchResult, RuntimeDebug,
|
||||
};
|
||||
|
||||
use self::meter::Diff;
|
||||
|
||||
/// Information for managing an account and its sub trie abstraction.
|
||||
/// This is the required info to cache for an account.
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct ContractInfo<T: Config> {
|
||||
/// Unique ID for the subtree encoded as a bytes vector.
|
||||
pub trie_id: TrieId,
|
||||
/// The code associated with a given account.
|
||||
pub code_hash: CodeHash<T>,
|
||||
/// How many bytes of storage are accumulated in this contract's child trie.
|
||||
storage_bytes: u32,
|
||||
/// How many items of storage are accumulated in this contract's child trie.
|
||||
storage_items: u32,
|
||||
/// This records to how much deposit the accumulated `storage_bytes` amount to.
|
||||
pub storage_byte_deposit: BalanceOf<T>,
|
||||
/// This records to how much deposit the accumulated `storage_items` amount to.
|
||||
storage_item_deposit: BalanceOf<T>,
|
||||
/// This records how much deposit is put down in order to pay for the contract itself.
|
||||
///
|
||||
/// We need to store this information separately so it is not used when calculating any refunds
|
||||
/// since the base deposit can only ever be refunded on contract termination.
|
||||
storage_base_deposit: BalanceOf<T>,
|
||||
/// Map of code hashes and deposit balances.
|
||||
///
|
||||
/// Tracks the code hash and deposit held for locking delegate dependencies. Dependencies added
|
||||
/// to the map can not be removed from the chain state and can be safely used for delegate
|
||||
/// calls.
|
||||
delegate_dependencies: BoundedBTreeMap<CodeHash<T>, BalanceOf<T>, T::MaxDelegateDependencies>,
|
||||
}
|
||||
|
||||
impl<T: Config> ContractInfo<T> {
|
||||
/// Constructs a new contract info **without** writing it to storage.
|
||||
///
|
||||
/// This returns an `Err` if an contract with the supplied `account` already exists
|
||||
/// in storage.
|
||||
pub fn new(
|
||||
account: &AccountIdOf<T>,
|
||||
nonce: u64,
|
||||
code_hash: CodeHash<T>,
|
||||
) -> Result<Self, DispatchError> {
|
||||
if <ContractInfoOf<T>>::contains_key(account) {
|
||||
return Err(Error::<T>::DuplicateContract.into());
|
||||
}
|
||||
|
||||
let trie_id = {
|
||||
let buf = (account, nonce).using_encoded(T::Hashing::hash);
|
||||
buf.as_ref()
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("Runtime uses a reasonable hash size. Hence sizeof(T::Hash) <= 128; qed")
|
||||
};
|
||||
|
||||
let contract = Self {
|
||||
trie_id,
|
||||
code_hash,
|
||||
storage_bytes: 0,
|
||||
storage_items: 0,
|
||||
storage_byte_deposit: Zero::zero(),
|
||||
storage_item_deposit: Zero::zero(),
|
||||
storage_base_deposit: Zero::zero(),
|
||||
delegate_dependencies: Default::default(),
|
||||
};
|
||||
|
||||
Ok(contract)
|
||||
}
|
||||
|
||||
/// Returns the number of locked delegate dependencies.
|
||||
pub fn delegate_dependencies_count(&self) -> usize {
|
||||
self.delegate_dependencies.len()
|
||||
}
|
||||
|
||||
/// Associated child trie unique id is built from the hash part of the trie id.
|
||||
pub fn child_trie_info(&self) -> ChildInfo {
|
||||
ChildInfo::new_default(self.trie_id.as_ref())
|
||||
}
|
||||
|
||||
/// The deposit paying for the accumulated storage generated within the contract's child trie.
|
||||
pub fn extra_deposit(&self) -> BalanceOf<T> {
|
||||
self.storage_byte_deposit.saturating_add(self.storage_item_deposit)
|
||||
}
|
||||
|
||||
/// Same as [`Self::extra_deposit`] but including the base deposit.
|
||||
pub fn total_deposit(&self) -> BalanceOf<T> {
|
||||
self.extra_deposit().saturating_add(self.storage_base_deposit)
|
||||
}
|
||||
|
||||
/// Returns the storage base deposit of the contract.
|
||||
pub fn storage_base_deposit(&self) -> BalanceOf<T> {
|
||||
self.storage_base_deposit
|
||||
}
|
||||
|
||||
/// Reads a storage kv pair of a contract.
|
||||
///
|
||||
/// The read is performed from the `trie_id` only. The `address` is not necessary. If the
|
||||
/// contract doesn't store under the given `key` `None` is returned.
|
||||
pub fn read(&self, key: &Key<T>) -> Option<Vec<u8>> {
|
||||
child::get_raw(&self.child_trie_info(), key.hash().as_slice())
|
||||
}
|
||||
|
||||
/// Returns `Some(len)` (in bytes) if a storage item exists at `key`.
|
||||
///
|
||||
/// Returns `None` if the `key` wasn't previously set by `set_storage` or
|
||||
/// was deleted.
|
||||
pub fn size(&self, key: &Key<T>) -> Option<u32> {
|
||||
child::len(&self.child_trie_info(), key.hash().as_slice())
|
||||
}
|
||||
|
||||
/// Update a storage entry into a contract's kv storage.
|
||||
///
|
||||
/// If the `new_value` is `None` then the kv pair is removed. If `take` is true
|
||||
/// a [`WriteOutcome::Taken`] is returned instead of a [`WriteOutcome::Overwritten`].
|
||||
///
|
||||
/// This function also records how much storage was created or removed if a `storage_meter`
|
||||
/// is supplied. It should only be absent for testing or benchmarking code.
|
||||
pub fn write(
|
||||
&self,
|
||||
key: &Key<T>,
|
||||
new_value: Option<Vec<u8>>,
|
||||
storage_meter: Option<&mut meter::NestedMeter<T>>,
|
||||
take: bool,
|
||||
) -> Result<WriteOutcome, DispatchError> {
|
||||
let hashed_key = key.hash();
|
||||
self.write_raw(&hashed_key, new_value, storage_meter, take)
|
||||
}
|
||||
|
||||
/// Update a storage entry into a contract's kv storage.
|
||||
/// Function used in benchmarks, which can simulate prefix collision in keys.
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn bench_write_raw(
|
||||
&self,
|
||||
key: &[u8],
|
||||
new_value: Option<Vec<u8>>,
|
||||
take: bool,
|
||||
) -> Result<WriteOutcome, DispatchError> {
|
||||
self.write_raw(key, new_value, None, take)
|
||||
}
|
||||
|
||||
fn write_raw(
|
||||
&self,
|
||||
key: &[u8],
|
||||
new_value: Option<Vec<u8>>,
|
||||
storage_meter: Option<&mut meter::NestedMeter<T>>,
|
||||
take: bool,
|
||||
) -> Result<WriteOutcome, DispatchError> {
|
||||
let child_trie_info = &self.child_trie_info();
|
||||
let (old_len, old_value) = if take {
|
||||
let val = child::get_raw(child_trie_info, key);
|
||||
(val.as_ref().map(|v| v.len() as u32), val)
|
||||
} else {
|
||||
(child::len(child_trie_info, key), None)
|
||||
};
|
||||
|
||||
if let Some(storage_meter) = storage_meter {
|
||||
let mut diff = meter::Diff::default();
|
||||
match (old_len, new_value.as_ref().map(|v| v.len() as u32)) {
|
||||
(Some(old_len), Some(new_len)) =>
|
||||
if new_len > old_len {
|
||||
diff.bytes_added = new_len - old_len;
|
||||
} else {
|
||||
diff.bytes_removed = old_len - new_len;
|
||||
},
|
||||
(None, Some(new_len)) => {
|
||||
diff.bytes_added = new_len;
|
||||
diff.items_added = 1;
|
||||
},
|
||||
(Some(old_len), None) => {
|
||||
diff.bytes_removed = old_len;
|
||||
diff.items_removed = 1;
|
||||
},
|
||||
(None, None) => (),
|
||||
}
|
||||
storage_meter.charge(&diff);
|
||||
}
|
||||
|
||||
match &new_value {
|
||||
Some(new_value) => child::put_raw(child_trie_info, key, new_value),
|
||||
None => child::kill(child_trie_info, key),
|
||||
}
|
||||
|
||||
Ok(match (old_len, old_value) {
|
||||
(None, _) => WriteOutcome::New,
|
||||
(Some(old_len), None) => WriteOutcome::Overwritten(old_len),
|
||||
(Some(_), Some(old_value)) => WriteOutcome::Taken(old_value),
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets and returns the contract base deposit.
|
||||
///
|
||||
/// The base deposit is updated when the `code_hash` of the contract changes, as it depends on
|
||||
/// the deposit paid to upload the contract's code.
|
||||
pub fn update_base_deposit(&mut self, code_info: &CodeInfo<T>) -> BalanceOf<T> {
|
||||
let info_deposit =
|
||||
Diff { bytes_added: self.encoded_size() as u32, items_added: 1, ..Default::default() }
|
||||
.update_contract::<T>(None)
|
||||
.charge_or_zero();
|
||||
|
||||
// Instantiating the contract prevents its code to be deleted, therefore the base deposit
|
||||
// includes a fraction (`T::CodeHashLockupDepositPercent`) of the original storage deposit
|
||||
// to prevent abuse.
|
||||
let upload_deposit = T::CodeHashLockupDepositPercent::get().mul_ceil(code_info.deposit());
|
||||
|
||||
let deposit = info_deposit.saturating_add(upload_deposit);
|
||||
self.storage_base_deposit = deposit;
|
||||
deposit
|
||||
}
|
||||
|
||||
/// Adds a new delegate dependency to the contract.
|
||||
/// The `amount` is the amount of funds that will be reserved for the dependency.
|
||||
///
|
||||
/// Returns an error if the maximum number of delegate_dependencies is reached or if
|
||||
/// the delegate dependency already exists.
|
||||
pub fn lock_delegate_dependency(
|
||||
&mut self,
|
||||
code_hash: CodeHash<T>,
|
||||
amount: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
self.delegate_dependencies
|
||||
.try_insert(code_hash, amount)
|
||||
.map_err(|_| Error::<T>::MaxDelegateDependenciesReached)?
|
||||
.map_or(Ok(()), |_| Err(Error::<T>::DelegateDependencyAlreadyExists))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Removes the delegate dependency from the contract and returns the deposit held for this
|
||||
/// dependency.
|
||||
///
|
||||
/// Returns an error if the entry doesn't exist.
|
||||
pub fn unlock_delegate_dependency(
|
||||
&mut self,
|
||||
code_hash: &CodeHash<T>,
|
||||
) -> Result<BalanceOf<T>, DispatchError> {
|
||||
self.delegate_dependencies
|
||||
.remove(code_hash)
|
||||
.ok_or(Error::<T>::DelegateDependencyNotFound.into())
|
||||
}
|
||||
|
||||
/// Returns the delegate_dependencies of the contract.
|
||||
pub fn delegate_dependencies(
|
||||
&self,
|
||||
) -> &BoundedBTreeMap<CodeHash<T>, BalanceOf<T>, T::MaxDelegateDependencies> {
|
||||
&self.delegate_dependencies
|
||||
}
|
||||
|
||||
/// Push a contract's trie to the deletion queue for lazy removal.
|
||||
///
|
||||
/// You must make sure that the contract is also removed when queuing the trie for deletion.
|
||||
pub fn queue_trie_for_deletion(&self) {
|
||||
DeletionQueueManager::<T>::load().insert(self.trie_id.clone());
|
||||
}
|
||||
|
||||
/// Calculates the weight that is necessary to remove one key from the trie and how many
|
||||
/// of those keys can be deleted from the deletion queue given the supplied weight limit.
|
||||
pub fn deletion_budget(meter: &WeightMeter) -> (Weight, u32) {
|
||||
let base_weight = T::WeightInfo::on_process_deletion_queue_batch();
|
||||
let weight_per_key = T::WeightInfo::on_initialize_per_trie_key(1) -
|
||||
T::WeightInfo::on_initialize_per_trie_key(0);
|
||||
|
||||
// `weight_per_key` being zero makes no sense and would constitute a failure to
|
||||
// benchmark properly. We opt for not removing any keys at all in this case.
|
||||
let key_budget = meter
|
||||
.limit()
|
||||
.saturating_sub(base_weight)
|
||||
.checked_div_per_component(&weight_per_key)
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
(weight_per_key, key_budget)
|
||||
}
|
||||
|
||||
/// Delete as many items from the deletion queue possible within the supplied weight limit.
|
||||
pub fn process_deletion_queue_batch(meter: &mut WeightMeter) {
|
||||
if meter.try_consume(T::WeightInfo::on_process_deletion_queue_batch()).is_err() {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut queue = <DeletionQueueManager<T>>::load();
|
||||
if queue.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (weight_per_key, budget) = Self::deletion_budget(&meter);
|
||||
let mut remaining_key_budget = budget;
|
||||
while remaining_key_budget > 0 {
|
||||
let Some(entry) = queue.next() else { break };
|
||||
|
||||
#[allow(deprecated)]
|
||||
let outcome = child::kill_storage(
|
||||
&ChildInfo::new_default(&entry.trie_id),
|
||||
Some(remaining_key_budget),
|
||||
);
|
||||
|
||||
match outcome {
|
||||
// This happens when our budget wasn't large enough to remove all keys.
|
||||
KillStorageResult::SomeRemaining(keys_removed) => {
|
||||
remaining_key_budget.saturating_reduce(keys_removed);
|
||||
break;
|
||||
},
|
||||
KillStorageResult::AllRemoved(keys_removed) => {
|
||||
entry.remove();
|
||||
// charge at least one key even if none were removed.
|
||||
remaining_key_budget = remaining_key_budget.saturating_sub(keys_removed.max(1));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
meter.consume(weight_per_key.saturating_mul(u64::from(budget - remaining_key_budget)))
|
||||
}
|
||||
|
||||
/// Returns the code hash of the contract specified by `account` ID.
|
||||
pub fn load_code_hash(account: &AccountIdOf<T>) -> Option<CodeHash<T>> {
|
||||
<ContractInfoOf<T>>::get(account).map(|i| i.code_hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about what happened to the pre-existing value when calling [`ContractInfo::write`].
|
||||
#[cfg_attr(any(test, feature = "runtime-benchmarks"), derive(Debug, PartialEq))]
|
||||
pub enum WriteOutcome {
|
||||
/// No value existed at the specified key.
|
||||
New,
|
||||
/// A value of the returned length was overwritten.
|
||||
Overwritten(u32),
|
||||
/// The returned value was taken out of storage before being overwritten.
|
||||
///
|
||||
/// This is only returned when specifically requested because it causes additional work
|
||||
/// depending on the size of the pre-existing value. When not requested [`Self::Overwritten`]
|
||||
/// is returned instead.
|
||||
Taken(Vec<u8>),
|
||||
}
|
||||
|
||||
impl WriteOutcome {
|
||||
/// Extracts the size of the overwritten value or `0` if there
|
||||
/// was no value in storage.
|
||||
pub fn old_len(&self) -> u32 {
|
||||
match self {
|
||||
Self::New => 0,
|
||||
Self::Overwritten(len) => *len,
|
||||
Self::Taken(value) => value.len() as u32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the size of the overwritten value or `SENTINEL` if there
|
||||
/// was no value in storage.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// We cannot use `0` as sentinel value because there could be a zero sized
|
||||
/// storage entry which is different from a non existing one.
|
||||
pub fn old_len_with_sentinel(&self) -> u32 {
|
||||
match self {
|
||||
Self::New => SENTINEL,
|
||||
Self::Overwritten(len) => *len,
|
||||
Self::Taken(value) => value.len() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Manage the removal of contracts storage that are marked for deletion.
|
||||
///
|
||||
/// When a contract is deleted by calling `seal_terminate` it becomes inaccessible
|
||||
/// immediately, but the deletion of the storage items it has accumulated is performed
|
||||
/// later by pulling the contract from the queue in the `on_idle` hook.
|
||||
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, DefaultNoBound, Clone)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct DeletionQueueManager<T: Config> {
|
||||
/// Counter used as a key for inserting a new deleted contract in the queue.
|
||||
/// The counter is incremented after each insertion.
|
||||
insert_counter: u32,
|
||||
/// The index used to read the next element to be deleted in the queue.
|
||||
/// The counter is incremented after each deletion.
|
||||
delete_counter: u32,
|
||||
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
/// View on a contract that is marked for deletion.
|
||||
struct DeletionQueueEntry<'a, T: Config> {
|
||||
/// the trie id of the contract to delete.
|
||||
trie_id: TrieId,
|
||||
|
||||
/// A mutable reference on the queue so that the contract can be removed, and none can be added
|
||||
/// or read in the meantime.
|
||||
queue: &'a mut DeletionQueueManager<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: Config> DeletionQueueEntry<'a, T> {
|
||||
/// Remove the contract from the deletion queue.
|
||||
fn remove(self) {
|
||||
<DeletionQueue<T>>::remove(self.queue.delete_counter);
|
||||
self.queue.delete_counter = self.queue.delete_counter.wrapping_add(1);
|
||||
<DeletionQueueCounter<T>>::set(self.queue.clone());
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> DeletionQueueManager<T> {
|
||||
/// Load the `DeletionQueueCounter`, so we can perform read or write operations on the
|
||||
/// DeletionQueue storage.
|
||||
fn load() -> Self {
|
||||
<DeletionQueueCounter<T>>::get()
|
||||
}
|
||||
|
||||
/// Returns `true` if the queue contains no elements.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.insert_counter.wrapping_sub(self.delete_counter) == 0
|
||||
}
|
||||
|
||||
/// Insert a contract in the deletion queue.
|
||||
fn insert(&mut self, trie_id: TrieId) {
|
||||
<DeletionQueue<T>>::insert(self.insert_counter, trie_id);
|
||||
self.insert_counter = self.insert_counter.wrapping_add(1);
|
||||
<DeletionQueueCounter<T>>::set(self.clone());
|
||||
}
|
||||
|
||||
/// Fetch the next contract to be deleted.
|
||||
///
|
||||
/// Note:
|
||||
/// we use the delete counter to get the next value to read from the queue and thus don't pay
|
||||
/// the cost of an extra call to `pezsp_io::storage::next_key` to lookup the next entry in the map
|
||||
fn next(&mut self) -> Option<DeletionQueueEntry<'_, T>> {
|
||||
if self.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let entry = <DeletionQueue<T>>::get(self.delete_counter);
|
||||
entry.map(|trie_id| DeletionQueueEntry { trie_id, queue: self })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<T: Config> DeletionQueueManager<T> {
|
||||
pub fn from_test_values(insert_counter: u32, delete_counter: u32) -> Self {
|
||||
Self { insert_counter, delete_counter, _phantom: Default::default() }
|
||||
}
|
||||
pub fn as_test_tuple(&self) -> (u32, u32) {
|
||||
(self.insert_counter, self.delete_counter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,908 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! This module contains functions to meter the storage deposit.
|
||||
|
||||
use crate::{
|
||||
storage::ContractInfo, AccountIdOf, BalanceOf, CodeInfo, Config, Error, Event, HoldReason,
|
||||
Inspect, Origin, Pallet, StorageDeposit as Deposit, System, LOG_TARGET,
|
||||
};
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use core::{fmt::Debug, marker::PhantomData};
|
||||
use pezframe_support::{
|
||||
ensure,
|
||||
traits::{
|
||||
fungible::{Mutate, MutateHold},
|
||||
tokens::{
|
||||
Fortitude, Fortitude::Polite, Precision, Preservation, Restriction, WithdrawConsequence,
|
||||
},
|
||||
Get,
|
||||
},
|
||||
DefaultNoBound, RuntimeDebugNoBound,
|
||||
};
|
||||
use pezsp_runtime::{
|
||||
traits::{Saturating, Zero},
|
||||
DispatchError, FixedPointNumber, FixedU128,
|
||||
};
|
||||
|
||||
/// Deposit that uses the native fungible's balance type.
|
||||
pub type DepositOf<T> = Deposit<BalanceOf<T>>;
|
||||
|
||||
/// A production root storage meter that actually charges from its origin.
|
||||
pub type Meter<T> = RawMeter<T, ReservingExt, Root>;
|
||||
|
||||
/// A production nested storage meter that actually charges from its origin.
|
||||
pub type NestedMeter<T> = RawMeter<T, ReservingExt, Nested>;
|
||||
|
||||
/// A production storage meter that actually charges from its origin.
|
||||
///
|
||||
/// This can be used where we want to be generic over the state (Root vs. Nested).
|
||||
pub type GenericMeter<T, S> = RawMeter<T, ReservingExt, S>;
|
||||
|
||||
/// A trait that allows to decouple the metering from the charging of balance.
|
||||
///
|
||||
/// This mostly exists for testing so that the charging can be mocked.
|
||||
pub trait Ext<T: Config> {
|
||||
/// This checks whether `origin` is able to afford the storage deposit limit.
|
||||
///
|
||||
/// It is necessary to do this check beforehand so that the charge won't fail later on.
|
||||
///
|
||||
/// `origin`: The origin of the call stack from which is responsible for putting down a deposit.
|
||||
/// `limit`: The limit with which the meter was constructed.
|
||||
/// `min_leftover`: How much `free_balance` in addition to the existential deposit (ed) should
|
||||
/// be left inside the `origin` account.
|
||||
///
|
||||
/// Returns the limit that should be used by the meter. If origin can't afford the `limit`
|
||||
/// it returns `Err`.
|
||||
fn check_limit(
|
||||
origin: &T::AccountId,
|
||||
limit: Option<BalanceOf<T>>,
|
||||
min_leftover: BalanceOf<T>,
|
||||
) -> Result<BalanceOf<T>, DispatchError>;
|
||||
/// This is called to inform the implementer that some balance should be charged due to
|
||||
/// some interaction of the `origin` with a `contract`.
|
||||
///
|
||||
/// The balance transfer can either flow from `origin` to `contract` or the other way
|
||||
/// around depending on whether `amount` constitutes a `Charge` or a `Refund`.
|
||||
/// It should be used in combination with `check_limit` to check that no more balance than this
|
||||
/// limit is ever charged.
|
||||
fn charge(
|
||||
origin: &T::AccountId,
|
||||
contract: &T::AccountId,
|
||||
amount: &DepositOf<T>,
|
||||
state: &ContractState<T>,
|
||||
) -> Result<(), DispatchError>;
|
||||
}
|
||||
|
||||
/// This [`Ext`] is used for actual on-chain execution when balance needs to be charged.
|
||||
///
|
||||
/// It uses [`pezframe_support::traits::fungible::Mutate`] in order to do accomplish the reserves.
|
||||
pub enum ReservingExt {}
|
||||
|
||||
/// Used to implement a type state pattern for the meter.
|
||||
///
|
||||
/// It is sealed and cannot be implemented outside of this module.
|
||||
pub trait State: private::Sealed {}
|
||||
|
||||
/// State parameter that constitutes a meter that is in its root state.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Root;
|
||||
|
||||
/// State parameter that constitutes a meter that is in its nested state.
|
||||
/// Its value indicates whether the nested meter has its own limit.
|
||||
#[derive(DefaultNoBound, RuntimeDebugNoBound)]
|
||||
pub enum Nested {
|
||||
#[default]
|
||||
DerivedLimit,
|
||||
OwnLimit,
|
||||
}
|
||||
|
||||
impl State for Root {}
|
||||
impl State for Nested {}
|
||||
|
||||
/// A type that allows the metering of consumed or freed storage of a single contract call stack.
|
||||
#[derive(DefaultNoBound, RuntimeDebugNoBound)]
|
||||
pub struct RawMeter<T: Config, E, S: State + Default + Debug> {
|
||||
/// The limit of how much balance this meter is allowed to consume.
|
||||
limit: BalanceOf<T>,
|
||||
/// The amount of balance that was used in this meter and all of its already absorbed children.
|
||||
total_deposit: DepositOf<T>,
|
||||
/// The amount of storage changes that were recorded in this meter alone.
|
||||
own_contribution: Contribution<T>,
|
||||
/// List of charges that should be applied at the end of a contract stack execution.
|
||||
///
|
||||
/// We only have one charge per contract hence the size of this vector is
|
||||
/// limited by the maximum call depth.
|
||||
charges: Vec<Charge<T>>,
|
||||
/// We store the nested state to determine if it has a special limit for sub-call.
|
||||
nested: S,
|
||||
/// Type parameter only used in impls.
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
/// This type is used to describe a storage change when charging from the meter.
|
||||
#[derive(Default, RuntimeDebugNoBound)]
|
||||
pub struct Diff {
|
||||
/// How many bytes were added to storage.
|
||||
pub bytes_added: u32,
|
||||
/// How many bytes were removed from storage.
|
||||
pub bytes_removed: u32,
|
||||
/// How many storage items were added to storage.
|
||||
pub items_added: u32,
|
||||
/// How many storage items were removed from storage.
|
||||
pub items_removed: u32,
|
||||
}
|
||||
|
||||
impl Diff {
|
||||
/// Calculate how much of a charge or refund results from applying the diff and store it
|
||||
/// in the passed `info` if any.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// In case `None` is passed for `info` only charges are calculated. This is because refunds
|
||||
/// are calculated pro rata of the existing storage within a contract and hence need extract
|
||||
/// this information from the passed `info`.
|
||||
pub fn update_contract<T: Config>(&self, info: Option<&mut ContractInfo<T>>) -> DepositOf<T> {
|
||||
let per_byte = T::DepositPerByte::get();
|
||||
let per_item = T::DepositPerItem::get();
|
||||
let bytes_added = self.bytes_added.saturating_sub(self.bytes_removed);
|
||||
let items_added = self.items_added.saturating_sub(self.items_removed);
|
||||
let mut bytes_deposit = Deposit::Charge(per_byte.saturating_mul((bytes_added).into()));
|
||||
let mut items_deposit = Deposit::Charge(per_item.saturating_mul((items_added).into()));
|
||||
|
||||
// Without any contract info we can only calculate diffs which add storage
|
||||
let info = if let Some(info) = info {
|
||||
info
|
||||
} else {
|
||||
debug_assert_eq!(self.bytes_removed, 0);
|
||||
debug_assert_eq!(self.items_removed, 0);
|
||||
return bytes_deposit.saturating_add(&items_deposit);
|
||||
};
|
||||
|
||||
// Refunds are calculated pro rata based on the accumulated storage within the contract
|
||||
let bytes_removed = self.bytes_removed.saturating_sub(self.bytes_added);
|
||||
let items_removed = self.items_removed.saturating_sub(self.items_added);
|
||||
let ratio = FixedU128::checked_from_rational(bytes_removed, info.storage_bytes)
|
||||
.unwrap_or_default()
|
||||
.min(FixedU128::from_u32(1));
|
||||
bytes_deposit = bytes_deposit
|
||||
.saturating_add(&Deposit::Refund(ratio.saturating_mul_int(info.storage_byte_deposit)));
|
||||
let ratio = FixedU128::checked_from_rational(items_removed, info.storage_items)
|
||||
.unwrap_or_default()
|
||||
.min(FixedU128::from_u32(1));
|
||||
items_deposit = items_deposit
|
||||
.saturating_add(&Deposit::Refund(ratio.saturating_mul_int(info.storage_item_deposit)));
|
||||
|
||||
// We need to update the contract info structure with the new deposits
|
||||
info.storage_bytes =
|
||||
info.storage_bytes.saturating_add(bytes_added).saturating_sub(bytes_removed);
|
||||
info.storage_items =
|
||||
info.storage_items.saturating_add(items_added).saturating_sub(items_removed);
|
||||
match &bytes_deposit {
|
||||
Deposit::Charge(amount) =>
|
||||
info.storage_byte_deposit = info.storage_byte_deposit.saturating_add(*amount),
|
||||
Deposit::Refund(amount) =>
|
||||
info.storage_byte_deposit = info.storage_byte_deposit.saturating_sub(*amount),
|
||||
}
|
||||
match &items_deposit {
|
||||
Deposit::Charge(amount) =>
|
||||
info.storage_item_deposit = info.storage_item_deposit.saturating_add(*amount),
|
||||
Deposit::Refund(amount) =>
|
||||
info.storage_item_deposit = info.storage_item_deposit.saturating_sub(*amount),
|
||||
}
|
||||
|
||||
bytes_deposit.saturating_add(&items_deposit)
|
||||
}
|
||||
}
|
||||
|
||||
impl Diff {
|
||||
fn saturating_add(&self, rhs: &Self) -> Self {
|
||||
Self {
|
||||
bytes_added: self.bytes_added.saturating_add(rhs.bytes_added),
|
||||
bytes_removed: self.bytes_removed.saturating_add(rhs.bytes_removed),
|
||||
items_added: self.items_added.saturating_add(rhs.items_added),
|
||||
items_removed: self.items_removed.saturating_add(rhs.items_removed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of a contract.
|
||||
///
|
||||
/// In case of termination the beneficiary is indicated.
|
||||
#[derive(RuntimeDebugNoBound, Clone, PartialEq, Eq)]
|
||||
pub enum ContractState<T: Config> {
|
||||
Alive,
|
||||
Terminated { beneficiary: AccountIdOf<T> },
|
||||
}
|
||||
|
||||
/// Records information to charge or refund a plain account.
|
||||
///
|
||||
/// All the charges are deferred to the end of a whole call stack. Reason is that by doing
|
||||
/// this we can do all the refunds before doing any charge. This way a plain account can use
|
||||
/// more deposit than it has balance as along as it is covered by a refund. This
|
||||
/// essentially makes the order of storage changes irrelevant with regard to the deposit system.
|
||||
/// The only exception is when a special (tougher) deposit limit is specified for a cross-contract
|
||||
/// call. In that case the limit is enforced once the call is returned, rolling it back if
|
||||
/// exhausted.
|
||||
#[derive(RuntimeDebugNoBound, Clone)]
|
||||
struct Charge<T: Config> {
|
||||
contract: T::AccountId,
|
||||
amount: DepositOf<T>,
|
||||
state: ContractState<T>,
|
||||
}
|
||||
|
||||
/// Records the storage changes of a storage meter.
|
||||
#[derive(RuntimeDebugNoBound)]
|
||||
enum Contribution<T: Config> {
|
||||
/// The contract the meter belongs to is alive and accumulates changes using a [`Diff`].
|
||||
Alive(Diff),
|
||||
/// The meter was checked against its limit using [`RawMeter::enforce_limit`] at the end of
|
||||
/// its execution. In this process the [`Diff`] was converted into a [`Deposit`].
|
||||
Checked(DepositOf<T>),
|
||||
/// The contract was terminated. In this process the [`Diff`] was converted into a [`Deposit`]
|
||||
/// in order to calculate the refund. Upon termination the `reducible_balance` in the
|
||||
/// contract's account is transferred to the [`beneficiary`].
|
||||
Terminated { deposit: DepositOf<T>, beneficiary: AccountIdOf<T> },
|
||||
}
|
||||
|
||||
impl<T: Config> Contribution<T> {
|
||||
/// See [`Diff::update_contract`].
|
||||
fn update_contract(&self, info: Option<&mut ContractInfo<T>>) -> DepositOf<T> {
|
||||
match self {
|
||||
Self::Alive(diff) => diff.update_contract::<T>(info),
|
||||
Self::Terminated { deposit, beneficiary: _ } | Self::Checked(deposit) =>
|
||||
deposit.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Default for Contribution<T> {
|
||||
fn default() -> Self {
|
||||
Self::Alive(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions that apply to all states.
|
||||
impl<T, E, S> RawMeter<T, E, S>
|
||||
where
|
||||
T: Config,
|
||||
E: Ext<T>,
|
||||
S: State + Default + Debug,
|
||||
{
|
||||
/// Create a new child that has its `limit`.
|
||||
/// Passing `0` as the limit is interpreted as to take whatever is remaining from its parent.
|
||||
///
|
||||
/// This is called whenever a new subcall is initiated in order to track the storage
|
||||
/// usage for this sub call separately. This is necessary because we want to exchange balance
|
||||
/// with the current contract we are interacting with.
|
||||
pub fn nested(&self, limit: BalanceOf<T>) -> RawMeter<T, E, Nested> {
|
||||
debug_assert!(matches!(self.contract_state(), ContractState::Alive));
|
||||
// If a special limit is specified higher than it is available,
|
||||
// we want to enforce the lesser limit to the nested meter, to fail in the sub-call.
|
||||
let limit = self.available().min(limit);
|
||||
if limit.is_zero() {
|
||||
RawMeter { limit: self.available(), ..Default::default() }
|
||||
} else {
|
||||
RawMeter { limit, nested: Nested::OwnLimit, ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Absorb a child that was spawned to handle a sub call.
|
||||
///
|
||||
/// This should be called whenever a sub call comes to its end and it is **not** reverted.
|
||||
/// This does the actual balance transfer from/to `origin` and `contract` based on the
|
||||
/// overall storage consumption of the call. It also updates the supplied contract info.
|
||||
///
|
||||
/// In case a contract reverted the child meter should just be dropped in order to revert
|
||||
/// any changes it recorded.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `absorbed`: The child storage meter that should be absorbed.
|
||||
/// - `origin`: The origin that spawned the original root meter.
|
||||
/// - `contract`: The contract's account that this sub call belongs to.
|
||||
/// - `info`: The info of the contract in question. `None` if the contract was terminated.
|
||||
pub fn absorb(
|
||||
&mut self,
|
||||
absorbed: RawMeter<T, E, Nested>,
|
||||
contract: &T::AccountId,
|
||||
info: Option<&mut ContractInfo<T>>,
|
||||
) {
|
||||
let own_deposit = absorbed.own_contribution.update_contract(info);
|
||||
self.total_deposit = self
|
||||
.total_deposit
|
||||
.saturating_add(&absorbed.total_deposit)
|
||||
.saturating_add(&own_deposit);
|
||||
self.charges.extend_from_slice(&absorbed.charges);
|
||||
if !own_deposit.is_zero() {
|
||||
self.charges.push(Charge {
|
||||
contract: contract.clone(),
|
||||
amount: own_deposit,
|
||||
state: absorbed.contract_state(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The amount of balance that is still available from the original `limit`.
|
||||
fn available(&self) -> BalanceOf<T> {
|
||||
self.total_deposit.available(&self.limit)
|
||||
}
|
||||
|
||||
/// Returns the state of the currently executed contract.
|
||||
fn contract_state(&self) -> ContractState<T> {
|
||||
match &self.own_contribution {
|
||||
Contribution::Terminated { deposit: _, beneficiary } =>
|
||||
ContractState::Terminated { beneficiary: beneficiary.clone() },
|
||||
_ => ContractState::Alive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions that only apply to the root state.
|
||||
impl<T, E> RawMeter<T, E, Root>
|
||||
where
|
||||
T: Config,
|
||||
E: Ext<T>,
|
||||
{
|
||||
/// Create new storage meter for the specified `origin` and `limit`.
|
||||
///
|
||||
/// This tries to [`Ext::check_limit`] on `origin` and fails if this is not possible.
|
||||
pub fn new(
|
||||
origin: &Origin<T>,
|
||||
limit: Option<BalanceOf<T>>,
|
||||
min_leftover: BalanceOf<T>,
|
||||
) -> Result<Self, DispatchError> {
|
||||
// Check the limit only if the origin is not root.
|
||||
return match origin {
|
||||
Origin::Root => Ok(Self {
|
||||
limit: limit.unwrap_or(T::DefaultDepositLimit::get()),
|
||||
..Default::default()
|
||||
}),
|
||||
Origin::Signed(o) => {
|
||||
let limit = E::check_limit(o, limit, min_leftover)?;
|
||||
Ok(Self { limit, ..Default::default() })
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// The total amount of deposit that should change hands as result of the execution
|
||||
/// that this meter was passed into. This will also perform all the charges accumulated
|
||||
/// in the whole contract stack.
|
||||
///
|
||||
/// This drops the root meter in order to make sure it is only called when the whole
|
||||
/// execution did finish.
|
||||
pub fn try_into_deposit(self, origin: &Origin<T>) -> Result<DepositOf<T>, DispatchError> {
|
||||
// Only refund or charge deposit if the origin is not root.
|
||||
let origin = match origin {
|
||||
Origin::Root => return Ok(Deposit::Charge(Zero::zero())),
|
||||
Origin::Signed(o) => o,
|
||||
};
|
||||
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Refund(_))) {
|
||||
E::charge(origin, &charge.contract, &charge.amount, &charge.state)?;
|
||||
}
|
||||
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Charge(_))) {
|
||||
E::charge(origin, &charge.contract, &charge.amount, &charge.state)?;
|
||||
}
|
||||
Ok(self.total_deposit)
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions that only apply to the nested state.
|
||||
impl<T, E> RawMeter<T, E, Nested>
|
||||
where
|
||||
T: Config,
|
||||
E: Ext<T>,
|
||||
{
|
||||
/// Charges `diff` from the meter.
|
||||
pub fn charge(&mut self, diff: &Diff) {
|
||||
match &mut self.own_contribution {
|
||||
Contribution::Alive(own) => *own = own.saturating_add(diff),
|
||||
_ => panic!("Charge is never called after termination; qed"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Adds a deposit charge.
|
||||
///
|
||||
/// Use this method instead of [`Self::charge`] when the charge is not the result of a storage
|
||||
/// change. This is the case when a `delegate_dependency` is added or removed, or when the
|
||||
/// `code_hash` is updated. [`Self::charge`] cannot be used here because we keep track of the
|
||||
/// deposit charge separately from the storage charge.
|
||||
pub fn charge_deposit(&mut self, contract: T::AccountId, amount: DepositOf<T>) {
|
||||
self.total_deposit = self.total_deposit.saturating_add(&amount);
|
||||
self.charges.push(Charge { contract, amount, state: ContractState::Alive });
|
||||
}
|
||||
|
||||
/// Charges from `origin` a storage deposit for contract instantiation.
|
||||
///
|
||||
/// This immediately transfers the balance in order to create the account.
|
||||
pub fn charge_instantiate(
|
||||
&mut self,
|
||||
origin: &T::AccountId,
|
||||
contract: &T::AccountId,
|
||||
contract_info: &mut ContractInfo<T>,
|
||||
code_info: &CodeInfo<T>,
|
||||
) -> Result<(), DispatchError> {
|
||||
debug_assert!(matches!(self.contract_state(), ContractState::Alive));
|
||||
|
||||
// We need to make sure that the contract's account exists.
|
||||
let ed = Pallet::<T>::min_balance();
|
||||
self.total_deposit = Deposit::Charge(ed);
|
||||
T::Currency::transfer(origin, contract, ed, Preservation::Preserve)?;
|
||||
|
||||
// A consumer is added at account creation and removed it on termination, otherwise the
|
||||
// runtime could remove the account. As long as a contract exists its account must exist.
|
||||
// With the consumer, a correct runtime cannot remove the account.
|
||||
System::<T>::inc_consumers(contract)?;
|
||||
|
||||
let deposit = contract_info.update_base_deposit(&code_info);
|
||||
let deposit = Deposit::Charge(deposit);
|
||||
|
||||
self.charge_deposit(contract.clone(), deposit);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call to tell the meter that the currently executing contract was terminated.
|
||||
///
|
||||
/// This will manipulate the meter so that all storage deposit accumulated in
|
||||
/// `contract_info` will be refunded to the `origin` of the meter. And the free
|
||||
/// (`reducible_balance`) will be sent to the `beneficiary`.
|
||||
pub fn terminate(&mut self, info: &ContractInfo<T>, beneficiary: T::AccountId) {
|
||||
debug_assert!(matches!(self.contract_state(), ContractState::Alive));
|
||||
self.own_contribution = Contribution::Terminated {
|
||||
deposit: Deposit::Refund(info.total_deposit()),
|
||||
beneficiary,
|
||||
};
|
||||
}
|
||||
|
||||
/// [`Self::charge`] does not enforce the storage limit since we want to do this check as late
|
||||
/// as possible to allow later refunds to offset earlier charges.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// We normally need to call this **once** for every call stack and not for every cross contract
|
||||
/// call. However, if a dedicated limit is specified for a sub-call, this needs to be called
|
||||
/// once the sub-call has returned. For this, the [`Self::enforce_subcall_limit`] wrapper is
|
||||
/// used.
|
||||
pub fn enforce_limit(
|
||||
&mut self,
|
||||
info: Option<&mut ContractInfo<T>>,
|
||||
) -> Result<(), DispatchError> {
|
||||
let deposit = self.own_contribution.update_contract(info);
|
||||
let total_deposit = self.total_deposit.saturating_add(&deposit);
|
||||
// We don't want to override a `Terminated` with a `Checked`.
|
||||
if matches!(self.contract_state(), ContractState::Alive) {
|
||||
self.own_contribution = Contribution::Checked(deposit);
|
||||
}
|
||||
if let Deposit::Charge(amount) = total_deposit {
|
||||
if amount > self.limit {
|
||||
return Err(<Error<T>>::StorageDepositLimitExhausted.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is a wrapper around [`Self::enforce_limit`] to use on the exit from a sub-call to
|
||||
/// enforce its special limit if needed.
|
||||
pub fn enforce_subcall_limit(
|
||||
&mut self,
|
||||
info: Option<&mut ContractInfo<T>>,
|
||||
) -> Result<(), DispatchError> {
|
||||
match self.nested {
|
||||
Nested::OwnLimit => self.enforce_limit(info),
|
||||
Nested::DerivedLimit => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Ext<T> for ReservingExt {
|
||||
fn check_limit(
|
||||
origin: &T::AccountId,
|
||||
limit: Option<BalanceOf<T>>,
|
||||
min_leftover: BalanceOf<T>,
|
||||
) -> Result<BalanceOf<T>, DispatchError> {
|
||||
// We are sending the `min_leftover` and the `min_balance` from the origin
|
||||
// account as part of a contract call. Hence origin needs to have those left over
|
||||
// as free balance after accounting for all deposits.
|
||||
let max = T::Currency::reducible_balance(origin, Preservation::Preserve, Polite)
|
||||
.saturating_sub(min_leftover)
|
||||
.saturating_sub(Pallet::<T>::min_balance());
|
||||
let default = max.min(T::DefaultDepositLimit::get());
|
||||
let limit = limit.unwrap_or(default);
|
||||
ensure!(
|
||||
limit <= max &&
|
||||
matches!(T::Currency::can_withdraw(origin, limit), WithdrawConsequence::Success),
|
||||
<Error<T>>::StorageDepositNotEnoughFunds,
|
||||
);
|
||||
Ok(limit)
|
||||
}
|
||||
|
||||
fn charge(
|
||||
origin: &T::AccountId,
|
||||
contract: &T::AccountId,
|
||||
amount: &DepositOf<T>,
|
||||
state: &ContractState<T>,
|
||||
) -> Result<(), DispatchError> {
|
||||
match amount {
|
||||
Deposit::Charge(amount) | Deposit::Refund(amount) if amount.is_zero() => return Ok(()),
|
||||
Deposit::Charge(amount) => {
|
||||
// This could fail if the `origin` does not have enough liquidity. Ideally, though,
|
||||
// this should have been checked before with `check_limit`.
|
||||
T::Currency::transfer_and_hold(
|
||||
&HoldReason::StorageDepositReserve.into(),
|
||||
origin,
|
||||
contract,
|
||||
*amount,
|
||||
Precision::Exact,
|
||||
Preservation::Preserve,
|
||||
Fortitude::Polite,
|
||||
)?;
|
||||
|
||||
Pallet::<T>::deposit_event(Event::StorageDepositTransferredAndHeld {
|
||||
from: origin.clone(),
|
||||
to: contract.clone(),
|
||||
amount: *amount,
|
||||
});
|
||||
},
|
||||
Deposit::Refund(amount) => {
|
||||
let transferred = T::Currency::transfer_on_hold(
|
||||
&HoldReason::StorageDepositReserve.into(),
|
||||
contract,
|
||||
origin,
|
||||
*amount,
|
||||
Precision::BestEffort,
|
||||
Restriction::Free,
|
||||
Fortitude::Polite,
|
||||
)?;
|
||||
|
||||
Pallet::<T>::deposit_event(Event::StorageDepositTransferredAndReleased {
|
||||
from: contract.clone(),
|
||||
to: origin.clone(),
|
||||
amount: transferred,
|
||||
});
|
||||
|
||||
if transferred < *amount {
|
||||
// This should never happen, if it does it means that there is a bug in the
|
||||
// runtime logic. In the rare case this happens we try to refund as much as we
|
||||
// can, thus the `Precision::BestEffort`.
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to repatriate full storage deposit {:?} from contract {:?} to origin {:?}. Transferred {:?}.",
|
||||
amount, contract, origin, transferred,
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
if let ContractState::<T>::Terminated { beneficiary } = state {
|
||||
System::<T>::dec_consumers(&contract);
|
||||
// Whatever is left in the contract is sent to the termination beneficiary.
|
||||
T::Currency::transfer(
|
||||
&contract,
|
||||
&beneficiary,
|
||||
T::Currency::reducible_balance(&contract, Preservation::Expendable, Polite),
|
||||
Preservation::Expendable,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
impl Sealed for super::Root {}
|
||||
impl Sealed for super::Nested {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
exec::AccountIdOf,
|
||||
tests::{Test, ALICE, BOB, CHARLIE},
|
||||
};
|
||||
use pezframe_support::parameter_types;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
type TestMeter = RawMeter<Test, TestExt, Root>;
|
||||
|
||||
parameter_types! {
|
||||
static TestExtTestValue: TestExt = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
struct LimitCheck {
|
||||
origin: AccountIdOf<Test>,
|
||||
limit: BalanceOf<Test>,
|
||||
min_leftover: BalanceOf<Test>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
struct Charge {
|
||||
origin: AccountIdOf<Test>,
|
||||
contract: AccountIdOf<Test>,
|
||||
amount: DepositOf<Test>,
|
||||
state: ContractState<Test>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct TestExt {
|
||||
limit_checks: Vec<LimitCheck>,
|
||||
charges: Vec<Charge>,
|
||||
}
|
||||
|
||||
impl TestExt {
|
||||
fn clear(&mut self) {
|
||||
self.limit_checks.clear();
|
||||
self.charges.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Ext<Test> for TestExt {
|
||||
fn check_limit(
|
||||
origin: &AccountIdOf<Test>,
|
||||
limit: Option<BalanceOf<Test>>,
|
||||
min_leftover: BalanceOf<Test>,
|
||||
) -> Result<BalanceOf<Test>, DispatchError> {
|
||||
let limit = limit.unwrap_or(42);
|
||||
TestExtTestValue::mutate(|ext| {
|
||||
ext.limit_checks
|
||||
.push(LimitCheck { origin: origin.clone(), limit, min_leftover })
|
||||
});
|
||||
Ok(limit)
|
||||
}
|
||||
|
||||
fn charge(
|
||||
origin: &AccountIdOf<Test>,
|
||||
contract: &AccountIdOf<Test>,
|
||||
amount: &DepositOf<Test>,
|
||||
state: &ContractState<Test>,
|
||||
) -> Result<(), DispatchError> {
|
||||
TestExtTestValue::mutate(|ext| {
|
||||
ext.charges.push(Charge {
|
||||
origin: origin.clone(),
|
||||
contract: contract.clone(),
|
||||
amount: amount.clone(),
|
||||
state: state.clone(),
|
||||
})
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_ext() {
|
||||
TestExtTestValue::mutate(|ext| ext.clear())
|
||||
}
|
||||
|
||||
struct ChargingTestCase {
|
||||
origin: Origin<Test>,
|
||||
deposit: DepositOf<Test>,
|
||||
expected: TestExt,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StorageInfo {
|
||||
bytes: u32,
|
||||
items: u32,
|
||||
bytes_deposit: BalanceOf<Test>,
|
||||
items_deposit: BalanceOf<Test>,
|
||||
}
|
||||
|
||||
fn new_info(info: StorageInfo) -> ContractInfo<Test> {
|
||||
ContractInfo::<Test> {
|
||||
trie_id: Default::default(),
|
||||
code_hash: Default::default(),
|
||||
storage_bytes: info.bytes,
|
||||
storage_items: info.items,
|
||||
storage_byte_deposit: info.bytes_deposit,
|
||||
storage_item_deposit: info.items_deposit,
|
||||
storage_base_deposit: Default::default(),
|
||||
delegate_dependencies: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_reserves_balance_works() {
|
||||
clear_ext();
|
||||
|
||||
TestMeter::new(&Origin::from_account_id(ALICE), Some(1_000), 0).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
TestExtTestValue::get(),
|
||||
TestExt {
|
||||
limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }],
|
||||
..Default::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_charge_works() {
|
||||
clear_ext();
|
||||
|
||||
let mut meter = TestMeter::new(&Origin::from_account_id(ALICE), Some(1_000), 0).unwrap();
|
||||
assert_eq!(meter.available(), 1_000);
|
||||
|
||||
// an empty charge does not create a `Charge` entry
|
||||
let mut nested0 = meter.nested(BalanceOf::<Test>::zero());
|
||||
nested0.charge(&Default::default());
|
||||
meter.absorb(nested0, &BOB, None);
|
||||
|
||||
assert_eq!(
|
||||
TestExtTestValue::get(),
|
||||
TestExt {
|
||||
limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }],
|
||||
..Default::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn charging_works() {
|
||||
let test_cases = vec![
|
||||
ChargingTestCase {
|
||||
origin: Origin::<Test>::from_account_id(ALICE),
|
||||
deposit: Deposit::Refund(28),
|
||||
expected: TestExt {
|
||||
limit_checks: vec![LimitCheck { origin: ALICE, limit: 100, min_leftover: 0 }],
|
||||
charges: vec![
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: CHARLIE,
|
||||
amount: Deposit::Refund(10),
|
||||
state: ContractState::Alive,
|
||||
},
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: CHARLIE,
|
||||
amount: Deposit::Refund(20),
|
||||
state: ContractState::Alive,
|
||||
},
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: BOB,
|
||||
amount: Deposit::Charge(2),
|
||||
state: ContractState::Alive,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
ChargingTestCase {
|
||||
origin: Origin::<Test>::Root,
|
||||
deposit: Deposit::Charge(0),
|
||||
expected: TestExt { limit_checks: vec![], charges: vec![] },
|
||||
},
|
||||
];
|
||||
|
||||
for test_case in test_cases {
|
||||
clear_ext();
|
||||
|
||||
let mut meter = TestMeter::new(&test_case.origin, Some(100), 0).unwrap();
|
||||
assert_eq!(meter.available(), 100);
|
||||
|
||||
let mut nested0_info = new_info(StorageInfo {
|
||||
bytes: 100,
|
||||
items: 5,
|
||||
bytes_deposit: 100,
|
||||
items_deposit: 10,
|
||||
});
|
||||
let mut nested0 = meter.nested(BalanceOf::<Test>::zero());
|
||||
nested0.charge(&Diff {
|
||||
bytes_added: 108,
|
||||
bytes_removed: 5,
|
||||
items_added: 1,
|
||||
items_removed: 2,
|
||||
});
|
||||
nested0.charge(&Diff { bytes_removed: 99, ..Default::default() });
|
||||
|
||||
let mut nested1_info = new_info(StorageInfo {
|
||||
bytes: 100,
|
||||
items: 10,
|
||||
bytes_deposit: 100,
|
||||
items_deposit: 20,
|
||||
});
|
||||
let mut nested1 = nested0.nested(BalanceOf::<Test>::zero());
|
||||
nested1.charge(&Diff { items_removed: 5, ..Default::default() });
|
||||
nested0.absorb(nested1, &CHARLIE, Some(&mut nested1_info));
|
||||
|
||||
let mut nested2_info = new_info(StorageInfo {
|
||||
bytes: 100,
|
||||
items: 7,
|
||||
bytes_deposit: 100,
|
||||
items_deposit: 20,
|
||||
});
|
||||
let mut nested2 = nested0.nested(BalanceOf::<Test>::zero());
|
||||
nested2.charge(&Diff { items_removed: 7, ..Default::default() });
|
||||
nested0.absorb(nested2, &CHARLIE, Some(&mut nested2_info));
|
||||
|
||||
nested0.enforce_limit(Some(&mut nested0_info)).unwrap();
|
||||
meter.absorb(nested0, &BOB, Some(&mut nested0_info));
|
||||
|
||||
assert_eq!(meter.try_into_deposit(&test_case.origin).unwrap(), test_case.deposit);
|
||||
|
||||
assert_eq!(nested0_info.extra_deposit(), 112);
|
||||
assert_eq!(nested1_info.extra_deposit(), 110);
|
||||
assert_eq!(nested2_info.extra_deposit(), 100);
|
||||
|
||||
assert_eq!(TestExtTestValue::get(), test_case.expected)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn termination_works() {
|
||||
let test_cases = vec![
|
||||
ChargingTestCase {
|
||||
origin: Origin::<Test>::from_account_id(ALICE),
|
||||
deposit: Deposit::Refund(108),
|
||||
expected: TestExt {
|
||||
limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }],
|
||||
charges: vec![
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: CHARLIE,
|
||||
amount: Deposit::Refund(120),
|
||||
state: ContractState::Terminated { beneficiary: CHARLIE },
|
||||
},
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: BOB,
|
||||
amount: Deposit::Charge(12),
|
||||
state: ContractState::Alive,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
ChargingTestCase {
|
||||
origin: Origin::<Test>::Root,
|
||||
deposit: Deposit::Charge(0),
|
||||
expected: TestExt { limit_checks: vec![], charges: vec![] },
|
||||
},
|
||||
];
|
||||
|
||||
for test_case in test_cases {
|
||||
clear_ext();
|
||||
|
||||
let mut meter = TestMeter::new(&test_case.origin, Some(1_000), 0).unwrap();
|
||||
assert_eq!(meter.available(), 1_000);
|
||||
|
||||
let mut nested0 = meter.nested(BalanceOf::<Test>::zero());
|
||||
nested0.charge(&Diff {
|
||||
bytes_added: 5,
|
||||
bytes_removed: 1,
|
||||
items_added: 3,
|
||||
items_removed: 1,
|
||||
});
|
||||
nested0.charge(&Diff { items_added: 2, ..Default::default() });
|
||||
|
||||
let mut nested1_info = new_info(StorageInfo {
|
||||
bytes: 100,
|
||||
items: 10,
|
||||
bytes_deposit: 100,
|
||||
items_deposit: 20,
|
||||
});
|
||||
let mut nested1 = nested0.nested(BalanceOf::<Test>::zero());
|
||||
nested1.charge(&Diff { items_removed: 5, ..Default::default() });
|
||||
nested1.charge(&Diff { bytes_added: 20, ..Default::default() });
|
||||
nested1.terminate(&nested1_info, CHARLIE);
|
||||
nested0.enforce_limit(Some(&mut nested1_info)).unwrap();
|
||||
nested0.absorb(nested1, &CHARLIE, None);
|
||||
|
||||
meter.absorb(nested0, &BOB, None);
|
||||
assert_eq!(meter.try_into_deposit(&test_case.origin).unwrap(), test_case.deposit);
|
||||
assert_eq!(TestExtTestValue::get(), test_case.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Shared utilities for testing contracts.
|
||||
//! This is not part of the tests module because it is made public for other crates to use.
|
||||
#![cfg(feature = "std")]
|
||||
use pezframe_support::weights::Weight;
|
||||
pub use pezsp_runtime::AccountId32;
|
||||
|
||||
pub const ALICE: AccountId32 = AccountId32::new([1u8; 32]);
|
||||
pub const BOB: AccountId32 = AccountId32::new([2u8; 32]);
|
||||
pub const CHARLIE: AccountId32 = AccountId32::new([3u8; 32]);
|
||||
pub const DJANGO: AccountId32 = AccountId32::new([4u8; 32]);
|
||||
|
||||
pub const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024);
|
||||
pub mod builder;
|
||||
@@ -0,0 +1,220 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::GAS_LIMIT;
|
||||
use crate::{
|
||||
AccountIdLookupOf, AccountIdOf, BalanceOf, Code, CodeHash, CollectEvents, Config,
|
||||
ContractExecResult, ContractInstantiateResult, DebugInfo, Determinism, EventRecordOf,
|
||||
ExecReturnValue, InstantiateReturnValue, OriginFor, Pallet, Weight,
|
||||
};
|
||||
use codec::{Encode, HasCompact};
|
||||
use core::fmt::Debug;
|
||||
use pezframe_support::pezpallet_prelude::DispatchResultWithPostInfo;
|
||||
use paste::paste;
|
||||
use scale_info::TypeInfo;
|
||||
|
||||
/// Helper macro to generate a builder for contract API calls.
|
||||
macro_rules! builder {
|
||||
// Entry point to generate a builder for the given method.
|
||||
(
|
||||
$method:ident($($field:ident: $type:ty,)*) -> $result:ty;
|
||||
$($extra:item)*
|
||||
) => {
|
||||
paste!{
|
||||
builder!([< $method:camel Builder >], $method($($field: $type,)* ) -> $result; $($extra)*);
|
||||
}
|
||||
};
|
||||
// Generate the builder struct and its methods.
|
||||
(
|
||||
$name:ident,
|
||||
$method:ident($($field:ident: $type:ty,)*) -> $result:ty;
|
||||
$($extra:item)*
|
||||
) => {
|
||||
#[doc = concat!("A builder to construct a ", stringify!($method), " call")]
|
||||
pub struct $name<T: Config> {
|
||||
$($field: $type,)*
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl<T: Config> $name<T>
|
||||
where
|
||||
<BalanceOf<T> as HasCompact>::Type: Clone + Eq + PartialEq + Debug + TypeInfo + Encode,
|
||||
{
|
||||
$(
|
||||
#[doc = concat!("Set the ", stringify!($field))]
|
||||
pub fn $field(mut self, value: $type) -> Self {
|
||||
self.$field = value;
|
||||
self
|
||||
}
|
||||
)*
|
||||
|
||||
#[doc = concat!("Build the ", stringify!($method), " call")]
|
||||
pub fn build(self) -> $result {
|
||||
Pallet::<T>::$method(
|
||||
$(self.$field,)*
|
||||
)
|
||||
}
|
||||
|
||||
$($extra)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder!(
|
||||
instantiate_with_code(
|
||||
origin: OriginFor<T>,
|
||||
value: BalanceOf<T>,
|
||||
gas_limit: Weight,
|
||||
storage_deposit_limit: Option<<BalanceOf<T> as codec::HasCompact>::Type>,
|
||||
code: Vec<u8>,
|
||||
data: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
) -> DispatchResultWithPostInfo;
|
||||
|
||||
/// Create an [`InstantiateWithCodeBuilder`] with default values.
|
||||
pub fn instantiate_with_code(origin: OriginFor<T>, code: Vec<u8>) -> Self {
|
||||
Self {
|
||||
origin: origin,
|
||||
value: 0u32.into(),
|
||||
gas_limit: GAS_LIMIT,
|
||||
storage_deposit_limit: None,
|
||||
code,
|
||||
data: vec![],
|
||||
salt: vec![],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
builder!(
|
||||
instantiate(
|
||||
origin: OriginFor<T>,
|
||||
value: BalanceOf<T>,
|
||||
gas_limit: Weight,
|
||||
storage_deposit_limit: Option<<BalanceOf<T> as codec::HasCompact>::Type>,
|
||||
code_hash: CodeHash<T>,
|
||||
data: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
) -> DispatchResultWithPostInfo;
|
||||
|
||||
/// Create an [`InstantiateBuilder`] with default values.
|
||||
pub fn instantiate(origin: OriginFor<T>, code_hash: CodeHash<T>) -> Self {
|
||||
Self {
|
||||
origin,
|
||||
value: 0u32.into(),
|
||||
gas_limit: GAS_LIMIT,
|
||||
storage_deposit_limit: None,
|
||||
code_hash,
|
||||
data: vec![],
|
||||
salt: vec![],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
builder!(
|
||||
bare_instantiate(
|
||||
origin: AccountIdOf<T>,
|
||||
value: BalanceOf<T>,
|
||||
gas_limit: Weight,
|
||||
storage_deposit_limit: Option<BalanceOf<T>>,
|
||||
code: Code<CodeHash<T>>,
|
||||
data: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
debug: DebugInfo,
|
||||
collect_events: CollectEvents,
|
||||
) -> ContractInstantiateResult<AccountIdOf<T>, BalanceOf<T>, EventRecordOf<T>>;
|
||||
|
||||
/// Build the instantiate call and unwrap the result.
|
||||
pub fn build_and_unwrap_result(self) -> InstantiateReturnValue<AccountIdOf<T>> {
|
||||
self.build().result.unwrap()
|
||||
}
|
||||
|
||||
/// Build the instantiate call and unwrap the account id.
|
||||
pub fn build_and_unwrap_account_id(self) -> AccountIdOf<T> {
|
||||
self.build().result.unwrap().account_id
|
||||
}
|
||||
|
||||
pub fn bare_instantiate(origin: AccountIdOf<T>, code: Code<CodeHash<T>>) -> Self {
|
||||
Self {
|
||||
origin,
|
||||
value: 0u32.into(),
|
||||
gas_limit: GAS_LIMIT,
|
||||
storage_deposit_limit: None,
|
||||
code,
|
||||
data: vec![],
|
||||
salt: vec![],
|
||||
debug: DebugInfo::Skip,
|
||||
collect_events: CollectEvents::Skip,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
builder!(
|
||||
call(
|
||||
origin: OriginFor<T>,
|
||||
dest: AccountIdLookupOf<T>,
|
||||
value: BalanceOf<T>,
|
||||
gas_limit: Weight,
|
||||
storage_deposit_limit: Option<<BalanceOf<T> as codec::HasCompact>::Type>,
|
||||
data: Vec<u8>,
|
||||
) -> DispatchResultWithPostInfo;
|
||||
|
||||
/// Create a [`CallBuilder`] with default values.
|
||||
pub fn call(origin: OriginFor<T>, dest: AccountIdLookupOf<T>) -> Self {
|
||||
CallBuilder {
|
||||
origin,
|
||||
dest,
|
||||
value: 0u32.into(),
|
||||
gas_limit: GAS_LIMIT,
|
||||
storage_deposit_limit: None,
|
||||
data: vec![],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
builder!(
|
||||
bare_call(
|
||||
origin: AccountIdOf<T>,
|
||||
dest: AccountIdOf<T>,
|
||||
value: BalanceOf<T>,
|
||||
gas_limit: Weight,
|
||||
storage_deposit_limit: Option<BalanceOf<T>>,
|
||||
data: Vec<u8>,
|
||||
debug: DebugInfo,
|
||||
collect_events: CollectEvents,
|
||||
determinism: Determinism,
|
||||
) -> ContractExecResult<BalanceOf<T>, EventRecordOf<T>>;
|
||||
|
||||
/// Build the call and unwrap the result.
|
||||
pub fn build_and_unwrap_result(self) -> ExecReturnValue {
|
||||
self.build().result.unwrap()
|
||||
}
|
||||
|
||||
/// Create a [`BareCallBuilder`] with default values.
|
||||
pub fn bare_call(origin: AccountIdOf<T>, dest: AccountIdOf<T>) -> Self {
|
||||
Self {
|
||||
origin,
|
||||
dest,
|
||||
value: 0u32.into(),
|
||||
gas_limit: GAS_LIMIT,
|
||||
storage_deposit_limit: None,
|
||||
data: vec![],
|
||||
debug: DebugInfo::Skip,
|
||||
collect_events: CollectEvents::Skip,
|
||||
determinism: Determinism::Enforced,
|
||||
}
|
||||
}
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[pezframe_support::pallet(dev_mode)]
|
||||
pub mod pallet {
|
||||
use pezframe_support::{
|
||||
dispatch::{Pays, PostDispatchInfo},
|
||||
ensure,
|
||||
pezpallet_prelude::DispatchResultWithPostInfo,
|
||||
weights::Weight,
|
||||
};
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Dummy function that overcharges the predispatch weight, allowing us to test the correct
|
||||
/// values of [`ContractResult::gas_consumed`] and [`ContractResult::gas_required`] in
|
||||
/// tests.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(*pre_charge)]
|
||||
pub fn overestimate_pre_charge(
|
||||
origin: OriginFor<T>,
|
||||
pre_charge: Weight,
|
||||
actual_weight: Weight,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
ensure_signed(origin)?;
|
||||
ensure!(pre_charge.any_gt(actual_weight), "pre_charge must be > actual_weight");
|
||||
Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
debug::{CallInterceptor, CallSpan, ExecResult, ExportedFunction, Tracing},
|
||||
primitives::ExecReturnValue,
|
||||
AccountIdOf,
|
||||
};
|
||||
use pezframe_support::traits::Currency;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
struct DebugFrame {
|
||||
contract_account: AccountId32,
|
||||
call: ExportedFunction,
|
||||
input: Vec<u8>,
|
||||
result: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static DEBUG_EXECUTION_TRACE: RefCell<Vec<DebugFrame>> = RefCell::new(Vec::new());
|
||||
static INTERCEPTED_ADDRESS: RefCell<Option<AccountId32>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
pub struct TestDebug;
|
||||
pub struct TestCallSpan {
|
||||
contract_account: AccountId32,
|
||||
call: ExportedFunction,
|
||||
input: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Tracing<Test> for TestDebug {
|
||||
type CallSpan = TestCallSpan;
|
||||
|
||||
fn new_call_span(
|
||||
contract_account: &AccountIdOf<Test>,
|
||||
entry_point: ExportedFunction,
|
||||
input_data: &[u8],
|
||||
) -> TestCallSpan {
|
||||
DEBUG_EXECUTION_TRACE.with(|d| {
|
||||
d.borrow_mut().push(DebugFrame {
|
||||
contract_account: contract_account.clone(),
|
||||
call: entry_point,
|
||||
input: input_data.to_vec(),
|
||||
result: None,
|
||||
})
|
||||
});
|
||||
TestCallSpan {
|
||||
contract_account: contract_account.clone(),
|
||||
call: entry_point,
|
||||
input: input_data.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallInterceptor<Test> for TestDebug {
|
||||
fn intercept_call(
|
||||
contract_address: &<Test as pezframe_system::Config>::AccountId,
|
||||
_entry_point: &ExportedFunction,
|
||||
_input_data: &[u8],
|
||||
) -> Option<ExecResult> {
|
||||
INTERCEPTED_ADDRESS.with(|i| {
|
||||
if i.borrow().as_ref() == Some(contract_address) {
|
||||
Some(Ok(ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![] }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CallSpan for TestCallSpan {
|
||||
fn after_call(self, output: &ExecReturnValue) {
|
||||
DEBUG_EXECUTION_TRACE.with(|d| {
|
||||
d.borrow_mut().push(DebugFrame {
|
||||
contract_account: self.contract_account,
|
||||
call: self.call,
|
||||
input: self.input,
|
||||
result: Some(output.data.clone()),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debugging_works() {
|
||||
let (wasm_caller, _) = compile_module::<Test>("call").unwrap();
|
||||
let (wasm_callee, _) = compile_module::<Test>("store_call").unwrap();
|
||||
|
||||
fn current_stack() -> Vec<DebugFrame> {
|
||||
DEBUG_EXECUTION_TRACE.with(|stack| stack.borrow().clone())
|
||||
}
|
||||
|
||||
fn deploy(wasm: Vec<u8>) -> AccountId32 {
|
||||
Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
Code::Upload(wasm),
|
||||
vec![],
|
||||
vec![],
|
||||
DebugInfo::Skip,
|
||||
CollectEvents::Skip,
|
||||
)
|
||||
.result
|
||||
.unwrap()
|
||||
.account_id
|
||||
}
|
||||
|
||||
fn constructor_frame(contract_account: &AccountId32, after: bool) -> DebugFrame {
|
||||
DebugFrame {
|
||||
contract_account: contract_account.clone(),
|
||||
call: ExportedFunction::Constructor,
|
||||
input: vec![],
|
||||
result: if after { Some(vec![]) } else { None },
|
||||
}
|
||||
}
|
||||
|
||||
fn call_frame(contract_account: &AccountId32, args: Vec<u8>, after: bool) -> DebugFrame {
|
||||
DebugFrame {
|
||||
contract_account: contract_account.clone(),
|
||||
call: ExportedFunction::Call,
|
||||
input: args,
|
||||
result: if after { Some(vec![]) } else { None },
|
||||
}
|
||||
}
|
||||
|
||||
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
|
||||
assert_eq!(current_stack(), vec![]);
|
||||
|
||||
let addr_caller = deploy(wasm_caller);
|
||||
let addr_callee = deploy(wasm_callee);
|
||||
|
||||
assert_eq!(
|
||||
current_stack(),
|
||||
vec![
|
||||
constructor_frame(&addr_caller, false),
|
||||
constructor_frame(&addr_caller, true),
|
||||
constructor_frame(&addr_callee, false),
|
||||
constructor_frame(&addr_callee, true),
|
||||
]
|
||||
);
|
||||
|
||||
let main_args = (100u32, &addr_callee.clone()).encode();
|
||||
let inner_args = (100u32).encode();
|
||||
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr_caller.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
main_args.clone()
|
||||
));
|
||||
|
||||
let stack_top = current_stack()[4..].to_vec();
|
||||
assert_eq!(
|
||||
stack_top,
|
||||
vec![
|
||||
call_frame(&addr_caller, main_args.clone(), false),
|
||||
call_frame(&addr_callee, inner_args.clone(), false),
|
||||
call_frame(&addr_callee, inner_args, true),
|
||||
call_frame(&addr_caller, main_args, true),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_interception_works() {
|
||||
let (wasm, _) = compile_module::<Test>("dummy").unwrap();
|
||||
|
||||
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
|
||||
let account_id = Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
Code::Upload(wasm),
|
||||
vec![],
|
||||
// some salt to ensure that the address of this contract is unique among all tests
|
||||
vec![0x41, 0x41, 0x41, 0x41],
|
||||
DebugInfo::Skip,
|
||||
CollectEvents::Skip,
|
||||
)
|
||||
.result
|
||||
.unwrap()
|
||||
.account_id;
|
||||
|
||||
// no interception yet
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
account_id.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
vec![],
|
||||
));
|
||||
|
||||
// intercept calls to this contract
|
||||
INTERCEPTED_ADDRESS.with(|i| *i.borrow_mut() = Some(account_id.clone()));
|
||||
|
||||
assert_err_ignore_postinfo!(
|
||||
Contracts::call(RuntimeOrigin::signed(ALICE), account_id, 0, GAS_LIMIT, None, vec![],),
|
||||
<Error<Test>>::ContractReverted,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! This module contains routines for accessing and altering a contract transient storage.
|
||||
|
||||
use crate::{
|
||||
exec::{AccountIdOf, Key},
|
||||
storage::WriteOutcome,
|
||||
Config, Error,
|
||||
};
|
||||
use alloc::{collections::BTreeMap, vec::Vec};
|
||||
use codec::Encode;
|
||||
use core::{marker::PhantomData, mem};
|
||||
use pezframe_support::DefaultNoBound;
|
||||
use pezsp_runtime::{DispatchError, DispatchResult, Saturating};
|
||||
|
||||
/// Meter entry tracks transaction allocations.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MeterEntry {
|
||||
/// Allocations made in the current transaction.
|
||||
pub amount: u32,
|
||||
/// Allocations limit in the current transaction.
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
impl MeterEntry {
|
||||
/// Create a new entry.
|
||||
fn new(limit: u32) -> Self {
|
||||
Self { limit, amount: Default::default() }
|
||||
}
|
||||
|
||||
/// Check if the allocated amount exceeds the limit.
|
||||
fn exceeds_limit(&self, amount: u32) -> bool {
|
||||
self.amount.saturating_add(amount) > self.limit
|
||||
}
|
||||
|
||||
/// Absorb the allocation amount of the nested entry into the current entry.
|
||||
fn absorb(&mut self, rhs: Self) {
|
||||
self.amount.saturating_accrue(rhs.amount)
|
||||
}
|
||||
}
|
||||
|
||||
// The storage meter enforces a limit for each transaction,
|
||||
// which is calculated as free_storage * (1 - 1/16) for each subsequent frame.
|
||||
#[derive(DefaultNoBound)]
|
||||
pub struct StorageMeter<T: Config> {
|
||||
nested_meters: Vec<MeterEntry>,
|
||||
root_meter: MeterEntry,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> StorageMeter<T> {
|
||||
const STORAGE_FRACTION_DENOMINATOR: u32 = 16;
|
||||
/// Create a new storage allocation meter.
|
||||
fn new(memory_limit: u32) -> Self {
|
||||
Self { root_meter: MeterEntry::new(memory_limit), ..Default::default() }
|
||||
}
|
||||
|
||||
/// Charge the allocated amount of transaction storage from the meter.
|
||||
fn charge(&mut self, amount: u32) -> DispatchResult {
|
||||
let meter = self.current_mut();
|
||||
if meter.exceeds_limit(amount) {
|
||||
return Err(Error::<T>::OutOfTransientStorage.into());
|
||||
}
|
||||
meter.amount.saturating_accrue(amount);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Revert a transaction meter.
|
||||
fn revert(&mut self) {
|
||||
self.nested_meters.pop().expect(
|
||||
"A call to revert a meter must be preceded by a corresponding call to start a meter;
|
||||
the code within this crate makes sure that this is always the case; qed",
|
||||
);
|
||||
}
|
||||
|
||||
/// Start a transaction meter.
|
||||
fn start(&mut self) {
|
||||
let meter = self.current();
|
||||
let mut transaction_limit = meter.limit.saturating_sub(meter.amount);
|
||||
if !self.nested_meters.is_empty() {
|
||||
// Allow use of (1 - 1/STORAGE_FRACTION_DENOMINATOR) of free storage for subsequent
|
||||
// calls.
|
||||
transaction_limit.saturating_reduce(
|
||||
transaction_limit.saturating_div(Self::STORAGE_FRACTION_DENOMINATOR),
|
||||
);
|
||||
}
|
||||
|
||||
self.nested_meters.push(MeterEntry::new(transaction_limit));
|
||||
}
|
||||
|
||||
/// Commit a transaction meter.
|
||||
fn commit(&mut self) {
|
||||
let transaction_meter = self.nested_meters.pop().expect(
|
||||
"A call to commit a meter must be preceded by a corresponding call to start a meter;
|
||||
the code within this crate makes sure that this is always the case; qed",
|
||||
);
|
||||
self.current_mut().absorb(transaction_meter)
|
||||
}
|
||||
|
||||
/// The total allocated amount of memory.
|
||||
#[cfg(test)]
|
||||
fn total_amount(&self) -> u32 {
|
||||
self.nested_meters
|
||||
.iter()
|
||||
.fold(self.root_meter.amount, |acc, e| acc.saturating_add(e.amount))
|
||||
}
|
||||
|
||||
/// A mutable reference to the current meter entry.
|
||||
pub fn current_mut(&mut self) -> &mut MeterEntry {
|
||||
self.nested_meters.last_mut().unwrap_or(&mut self.root_meter)
|
||||
}
|
||||
|
||||
/// A reference to the current meter entry.
|
||||
pub fn current(&self) -> &MeterEntry {
|
||||
self.nested_meters.last().unwrap_or(&self.root_meter)
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry representing a journal change.
|
||||
struct JournalEntry {
|
||||
key: Vec<u8>,
|
||||
prev_value: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl JournalEntry {
|
||||
/// Create a new change.
|
||||
fn new(key: Vec<u8>, prev_value: Option<Vec<u8>>) -> Self {
|
||||
Self { key, prev_value }
|
||||
}
|
||||
|
||||
/// Revert the change.
|
||||
fn revert(self, storage: &mut Storage) {
|
||||
storage.write(&self.key, self.prev_value);
|
||||
}
|
||||
}
|
||||
|
||||
/// A journal containing transient storage modifications.
|
||||
struct Journal(Vec<JournalEntry>);
|
||||
|
||||
impl Journal {
|
||||
/// Create a new journal.
|
||||
fn new() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
|
||||
/// Add a change to the journal.
|
||||
fn push(&mut self, entry: JournalEntry) {
|
||||
self.0.push(entry);
|
||||
}
|
||||
|
||||
/// Length of the journal.
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Roll back all journal changes until the chackpoint
|
||||
fn rollback(&mut self, storage: &mut Storage, checkpoint: usize) {
|
||||
self.0.drain(checkpoint..).rev().for_each(|entry| entry.revert(storage));
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage for maintaining the current transaction state.
|
||||
#[derive(Default)]
|
||||
struct Storage(BTreeMap<Vec<u8>, Vec<u8>>);
|
||||
|
||||
impl Storage {
|
||||
/// Read the storage entry.
|
||||
fn read(&self, key: &Vec<u8>) -> Option<Vec<u8>> {
|
||||
self.0.get(key).cloned()
|
||||
}
|
||||
|
||||
/// Write the storage entry.
|
||||
fn write(&mut self, key: &Vec<u8>, value: Option<Vec<u8>>) -> Option<Vec<u8>> {
|
||||
if let Some(value) = value {
|
||||
// Insert storage entry.
|
||||
self.0.insert(key.clone(), value)
|
||||
} else {
|
||||
// Remove storage entry.
|
||||
self.0.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transient storage behaves almost identically to regular storage but is discarded after each
|
||||
/// transaction. It consists of a `BTreeMap` for the current state, a journal of all changes, and a
|
||||
/// list of checkpoints. On entry to the `start_transaction` function, a marker (checkpoint) is
|
||||
/// added to the list. New values are written to the current state, and the previous value is
|
||||
/// recorded in the journal (`write`). When the `commit_transaction` function is called, the marker
|
||||
/// to the journal index (checkpoint) of when that call was entered is discarded.
|
||||
/// On `rollback_transaction`, all entries are reverted up to the last checkpoint.
|
||||
pub struct TransientStorage<T: Config> {
|
||||
// The storage and journal size is limited by the storage meter.
|
||||
storage: Storage,
|
||||
journal: Journal,
|
||||
// The size of the StorageMeter is limited by the stack depth.
|
||||
meter: StorageMeter<T>,
|
||||
// The size of the checkpoints is limited by the stack depth.
|
||||
checkpoints: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<T: Config> TransientStorage<T> {
|
||||
/// Create new transient storage with the supplied memory limit.
|
||||
pub fn new(memory_limit: u32) -> Self {
|
||||
TransientStorage {
|
||||
storage: Default::default(),
|
||||
journal: Journal::new(),
|
||||
checkpoints: Default::default(),
|
||||
meter: StorageMeter::new(memory_limit),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the storage value. If the entry does not exist, `None` is returned.
|
||||
pub fn read(&self, account: &AccountIdOf<T>, key: &Key<T>) -> Option<Vec<u8>> {
|
||||
self.storage.read(&Self::storage_key(&account.encode(), &key.hash()))
|
||||
}
|
||||
|
||||
/// Write a value to storage.
|
||||
///
|
||||
/// If the `value` is `None`, then the entry is removed. If `take` is true,
|
||||
/// a [`WriteOutcome::Taken`] is returned instead of a [`WriteOutcome::Overwritten`].
|
||||
/// If the entry did not exist, [`WriteOutcome::New`] is returned.
|
||||
pub fn write(
|
||||
&mut self,
|
||||
account: &AccountIdOf<T>,
|
||||
key: &Key<T>,
|
||||
value: Option<Vec<u8>>,
|
||||
take: bool,
|
||||
) -> Result<WriteOutcome, DispatchError> {
|
||||
let key = Self::storage_key(&account.encode(), &key.hash());
|
||||
let prev_value = self.storage.read(&key);
|
||||
// Skip if the same value is being set.
|
||||
if prev_value != value {
|
||||
// Calculate the allocation size.
|
||||
if let Some(value) = &value {
|
||||
// Charge the key, value and journal entry.
|
||||
// If a new value is written, a new journal entry is created. The previous value is
|
||||
// moved to the journal along with its key, and the new value is written to
|
||||
// storage.
|
||||
let key_len = key.capacity();
|
||||
let mut amount = value
|
||||
.capacity()
|
||||
.saturating_add(key_len)
|
||||
.saturating_add(mem::size_of::<JournalEntry>());
|
||||
if prev_value.is_none() {
|
||||
// Charge a new storage entry.
|
||||
// If there was no previous value, a new entry is added to storage (BTreeMap)
|
||||
// containing a Vec for the key and a Vec for the value. The value was already
|
||||
// included in the amount.
|
||||
amount.saturating_accrue(key_len.saturating_add(mem::size_of::<Vec<u8>>()));
|
||||
}
|
||||
self.meter.charge(amount as _)?;
|
||||
}
|
||||
self.storage.write(&key, value);
|
||||
// Update the journal.
|
||||
self.journal.push(JournalEntry::new(key, prev_value.clone()));
|
||||
}
|
||||
|
||||
Ok(match (take, prev_value) {
|
||||
(_, None) => WriteOutcome::New,
|
||||
(false, Some(prev_value)) => WriteOutcome::Overwritten(prev_value.len() as _),
|
||||
(true, Some(prev_value)) => WriteOutcome::Taken(prev_value),
|
||||
})
|
||||
}
|
||||
|
||||
/// Start a new nested transaction.
|
||||
///
|
||||
/// This allows to either commit or roll back all changes that are made after this call.
|
||||
/// For every transaction there must be a matching call to either `rollback_transaction`
|
||||
/// or `commit_transaction`.
|
||||
pub fn start_transaction(&mut self) {
|
||||
self.meter.start();
|
||||
self.checkpoints.push(self.journal.len());
|
||||
}
|
||||
|
||||
/// Rollback the last transaction started by `start_transaction`.
|
||||
///
|
||||
/// Any changes made during that transaction are discarded.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if there is no open transaction.
|
||||
pub fn rollback_transaction(&mut self) {
|
||||
let checkpoint = self
|
||||
.checkpoints
|
||||
.pop()
|
||||
.expect(
|
||||
"A call to rollback_transaction must be preceded by a corresponding call to start_transaction;
|
||||
the code within this crate makes sure that this is always the case; qed"
|
||||
);
|
||||
self.meter.revert();
|
||||
self.journal.rollback(&mut self.storage, checkpoint);
|
||||
}
|
||||
|
||||
/// Commit the last transaction started by `start_transaction`.
|
||||
///
|
||||
/// Any changes made during that transaction are committed.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if there is no open transaction.
|
||||
pub fn commit_transaction(&mut self) {
|
||||
self.checkpoints
|
||||
.pop()
|
||||
.expect(
|
||||
"A call to commit_transaction must be preceded by a corresponding call to start_transaction;
|
||||
the code within this crate makes sure that this is always the case; qed"
|
||||
);
|
||||
self.meter.commit();
|
||||
}
|
||||
|
||||
/// The storage allocation meter used for transaction metering.
|
||||
#[cfg(any(test, feature = "runtime-benchmarks"))]
|
||||
pub fn meter(&mut self) -> &mut StorageMeter<T> {
|
||||
return &mut self.meter;
|
||||
}
|
||||
|
||||
fn storage_key(account: &[u8], key: &[u8]) -> Vec<u8> {
|
||||
let mut storage_key = Vec::with_capacity(account.len() + key.len());
|
||||
storage_key.extend_from_slice(&account);
|
||||
storage_key.extend_from_slice(&key);
|
||||
storage_key
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
tests::{Test, ALICE, BOB, CHARLIE},
|
||||
Error,
|
||||
};
|
||||
use core::u32::MAX;
|
||||
|
||||
// Calculate the allocation size for the given entry.
|
||||
fn allocation_size(
|
||||
account: &AccountIdOf<Test>,
|
||||
key: &Key<Test>,
|
||||
value: Option<Vec<u8>>,
|
||||
) -> u32 {
|
||||
let mut storage: TransientStorage<Test> = TransientStorage::<Test>::new(MAX);
|
||||
storage
|
||||
.write(account, key, value, false)
|
||||
.expect("Could not write to transient storage.");
|
||||
storage.meter().current().amount
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_works() {
|
||||
let mut storage: TransientStorage<Test> = TransientStorage::<Test>::new(2048);
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![2]), true),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
assert_eq!(
|
||||
storage.write(&BOB, &Key::Fix([3; 32]), Some(vec![3]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1]));
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([2; 32])), Some(vec![2]));
|
||||
assert_eq!(storage.read(&BOB, &Key::Fix([3; 32])), Some(vec![3]));
|
||||
// Overwrite values.
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![4, 5]), false),
|
||||
Ok(WriteOutcome::Overwritten(1))
|
||||
);
|
||||
assert_eq!(
|
||||
storage.write(&BOB, &Key::Fix([3; 32]), Some(vec![6, 7]), true),
|
||||
Ok(WriteOutcome::Taken(vec![3]))
|
||||
);
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1]));
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([2; 32])), Some(vec![4, 5]));
|
||||
assert_eq!(storage.read(&BOB, &Key::Fix([3; 32])), Some(vec![6, 7]));
|
||||
|
||||
// Check for an empty value.
|
||||
assert_eq!(
|
||||
storage.write(&BOB, &Key::Fix([3; 32]), Some(vec![]), true),
|
||||
Ok(WriteOutcome::Taken(vec![6, 7]))
|
||||
);
|
||||
assert_eq!(storage.read(&BOB, &Key::Fix([3; 32])), Some(vec![]));
|
||||
|
||||
assert_eq!(
|
||||
storage.write(&BOB, &Key::Fix([3; 32]), None, true),
|
||||
Ok(WriteOutcome::Taken(vec![]))
|
||||
);
|
||||
assert_eq!(storage.read(&BOB, &Key::Fix([3; 32])), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_with_var_sized_keys_works() {
|
||||
let mut storage = TransientStorage::<Test>::new(2048);
|
||||
assert_eq!(
|
||||
storage.write(
|
||||
&ALICE,
|
||||
&Key::<Test>::try_from_var([1; 64].to_vec()).unwrap(),
|
||||
Some(vec![1]),
|
||||
false
|
||||
),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
assert_eq!(
|
||||
storage.write(
|
||||
&BOB,
|
||||
&Key::<Test>::try_from_var([2; 64].to_vec()).unwrap(),
|
||||
Some(vec![2, 3]),
|
||||
false
|
||||
),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
assert_eq!(
|
||||
storage.read(&ALICE, &Key::<Test>::try_from_var([1; 64].to_vec()).unwrap()),
|
||||
Some(vec![1])
|
||||
);
|
||||
assert_eq!(
|
||||
storage.read(&BOB, &Key::<Test>::try_from_var([2; 64].to_vec()).unwrap()),
|
||||
Some(vec![2, 3])
|
||||
);
|
||||
// Overwrite values.
|
||||
assert_eq!(
|
||||
storage.write(
|
||||
&ALICE,
|
||||
&Key::<Test>::try_from_var([1; 64].to_vec()).unwrap(),
|
||||
Some(vec![4, 5]),
|
||||
false
|
||||
),
|
||||
Ok(WriteOutcome::Overwritten(1))
|
||||
);
|
||||
assert_eq!(
|
||||
storage.read(&ALICE, &Key::<Test>::try_from_var([1; 64].to_vec()).unwrap()),
|
||||
Some(vec![4, 5])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_transaction_works() {
|
||||
let mut storage = TransientStorage::<Test>::new(1024);
|
||||
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.rollback_transaction();
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_transaction_works() {
|
||||
let mut storage = TransientStorage::<Test>::new(1024);
|
||||
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.commit_transaction();
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overwrite_and_commmit_transaction_works() {
|
||||
let mut storage = TransientStorage::<Test>::new(1024);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1, 2]), false),
|
||||
Ok(WriteOutcome::Overwritten(1))
|
||||
);
|
||||
storage.commit_transaction();
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1, 2]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_in_nested_transaction_works() {
|
||||
let mut storage = TransientStorage::<Test>::new(1024);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&BOB, &Key::Fix([1; 32]), Some(vec![1]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.rollback_transaction();
|
||||
storage.commit_transaction();
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1]));
|
||||
assert_eq!(storage.read(&BOB, &Key::Fix([1; 32])), None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_in_nested_transaction_works() {
|
||||
let mut storage = TransientStorage::<Test>::new(1024);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&BOB, &Key::Fix([1; 32]), Some(vec![2]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&CHARLIE, &Key::Fix([1; 32]), Some(vec![3]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.commit_transaction();
|
||||
storage.commit_transaction();
|
||||
storage.commit_transaction();
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), Some(vec![1]));
|
||||
assert_eq!(storage.read(&BOB, &Key::Fix([1; 32])), Some(vec![2]));
|
||||
assert_eq!(storage.read(&CHARLIE, &Key::Fix([1; 32])), Some(vec![3]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_all_transactions_works() {
|
||||
let mut storage = TransientStorage::<Test>::new(1024);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&BOB, &Key::Fix([1; 32]), Some(vec![2]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&CHARLIE, &Key::Fix([1; 32]), Some(vec![3]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.commit_transaction();
|
||||
storage.commit_transaction();
|
||||
storage.rollback_transaction();
|
||||
assert_eq!(storage.read(&ALICE, &Key::Fix([1; 32])), None);
|
||||
assert_eq!(storage.read(&BOB, &Key::Fix([1; 32])), None);
|
||||
assert_eq!(storage.read(&CHARLIE, &Key::Fix([1; 32])), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metering_transactions_works() {
|
||||
let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]));
|
||||
let mut storage = TransientStorage::<Test>::new(size * 2);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
let limit = storage.meter().current().limit;
|
||||
storage.commit_transaction();
|
||||
|
||||
storage.start_transaction();
|
||||
assert_eq!(storage.meter().current().limit, limit - size);
|
||||
assert_eq!(storage.meter().current().limit - storage.meter().current().amount, size);
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
assert_eq!(storage.meter().current().amount, size);
|
||||
storage.commit_transaction();
|
||||
assert_eq!(storage.meter().total_amount(), size * 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metering_nested_transactions_works() {
|
||||
let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]));
|
||||
let mut storage = TransientStorage::<Test>::new(size * 3);
|
||||
|
||||
storage.start_transaction();
|
||||
let limit = storage.meter().current().limit;
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.start_transaction();
|
||||
assert_eq!(storage.meter().total_amount(), size);
|
||||
assert!(storage.meter().current().limit < limit - size);
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.commit_transaction();
|
||||
assert_eq!(storage.meter().current().limit, limit);
|
||||
assert_eq!(storage.meter().total_amount(), storage.meter().current().amount);
|
||||
storage.commit_transaction();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metering_transaction_fails() {
|
||||
let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]));
|
||||
let mut storage = TransientStorage::<Test>::new(size - 1);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false),
|
||||
Err(Error::<Test>::OutOfTransientStorage.into())
|
||||
);
|
||||
assert_eq!(storage.meter.current().amount, 0);
|
||||
storage.commit_transaction();
|
||||
assert_eq!(storage.meter.total_amount(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metering_nested_transactions_fails() {
|
||||
let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]));
|
||||
let mut storage = TransientStorage::<Test>::new(size * 2);
|
||||
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false),
|
||||
Err(Error::<Test>::OutOfTransientStorage.into())
|
||||
);
|
||||
storage.commit_transaction();
|
||||
storage.commit_transaction();
|
||||
assert_eq!(storage.meter.total_amount(), size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metering_nested_transaction_with_rollback_works() {
|
||||
let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]));
|
||||
let mut storage = TransientStorage::<Test>::new(size * 2);
|
||||
|
||||
storage.start_transaction();
|
||||
let limit = storage.meter.current().limit;
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.rollback_transaction();
|
||||
|
||||
assert_eq!(storage.meter.total_amount(), 0);
|
||||
assert_eq!(storage.meter.current().limit, limit);
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
let amount = storage.meter().current().amount;
|
||||
assert_eq!(storage.meter().total_amount(), amount);
|
||||
storage.commit_transaction();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metering_with_rollback_works() {
|
||||
let size = allocation_size(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]));
|
||||
let mut storage = TransientStorage::<Test>::new(size * 5);
|
||||
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
let amount = storage.meter.total_amount();
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&ALICE, &Key::Fix([2; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.start_transaction();
|
||||
assert_eq!(
|
||||
storage.write(&BOB, &Key::Fix([1; 32]), Some(vec![1u8; 4096]), false),
|
||||
Ok(WriteOutcome::New)
|
||||
);
|
||||
storage.commit_transaction();
|
||||
storage.rollback_transaction();
|
||||
assert_eq!(storage.meter.total_amount(), amount);
|
||||
storage.commit_transaction();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,876 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! This module takes care of loading, checking and preprocessing of a
|
||||
//! wasm module before execution. It also extracts some essential information
|
||||
//! from a module.
|
||||
|
||||
use crate::{
|
||||
chain_extension::ChainExtension,
|
||||
storage::meter::Diff,
|
||||
wasm::{
|
||||
runtime::AllowDeprecatedInterface, CodeInfo, Determinism, Environment, WasmBlob,
|
||||
BYTES_PER_PAGE,
|
||||
},
|
||||
AccountIdOf, CodeVec, Config, Error, Schedule, LOG_TARGET,
|
||||
};
|
||||
#[cfg(any(test, feature = "runtime-benchmarks"))]
|
||||
use alloc::vec::Vec;
|
||||
use codec::MaxEncodedLen;
|
||||
use pezsp_runtime::{traits::Hash, DispatchError};
|
||||
use wasmi::{
|
||||
core::ValType as WasmiValueType, CompilationMode, Config as WasmiConfig, Engine, ExternType,
|
||||
Module, StackLimits,
|
||||
};
|
||||
|
||||
/// Imported memory must be located inside this module. The reason for hardcoding is that current
|
||||
/// compiler toolchains might not support specifying other modules than "env" for memory imports.
|
||||
pub const IMPORT_MODULE_MEMORY: &str = "env";
|
||||
|
||||
/// The inner deserialized module is valid and contains only allowed WebAssembly features.
|
||||
/// This is checked by loading it into wasmi interpreter `engine`.
|
||||
pub struct LoadedModule {
|
||||
pub module: Module,
|
||||
pub engine: Engine,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum LoadingMode {
|
||||
Checked,
|
||||
Unchecked,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tracker {
|
||||
use core::cell::RefCell;
|
||||
thread_local! {
|
||||
pub static LOADED_MODULE: RefCell<Vec<super::LoadingMode>> = RefCell::new(Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl LoadedModule {
|
||||
/// Creates a new instance of `LoadedModule`.
|
||||
///
|
||||
/// The inner Wasm module is checked not to have restricted WebAssembly proposals.
|
||||
/// Returns `Err` if the `code` cannot be deserialized or if it contains an invalid module.
|
||||
pub fn new<T>(
|
||||
code: &[u8],
|
||||
determinism: Determinism,
|
||||
stack_limits: Option<StackLimits>,
|
||||
loading_mode: LoadingMode,
|
||||
compilation_mode: CompilationMode,
|
||||
) -> Result<Self, &'static str> {
|
||||
// NOTE: wasmi does not support unstable WebAssembly features. The module is implicitly
|
||||
// checked for not having those ones when creating `wasmi::Module` below.
|
||||
let mut config = WasmiConfig::default();
|
||||
config
|
||||
.wasm_multi_value(false)
|
||||
.wasm_mutable_global(false)
|
||||
.wasm_sign_extension(true)
|
||||
.wasm_bulk_memory(false)
|
||||
.wasm_reference_types(false)
|
||||
.wasm_tail_call(false)
|
||||
.wasm_extended_const(false)
|
||||
.wasm_saturating_float_to_int(false)
|
||||
.floats(matches!(determinism, Determinism::Relaxed))
|
||||
.compilation_mode(compilation_mode)
|
||||
.consume_fuel(true);
|
||||
|
||||
if let Some(stack_limits) = stack_limits {
|
||||
config.set_stack_limits(stack_limits);
|
||||
}
|
||||
|
||||
let engine = Engine::new(&config);
|
||||
|
||||
let module = match loading_mode {
|
||||
LoadingMode::Checked => Module::new(&engine, code),
|
||||
// Safety: The code has been validated, Therefore we know that it's a valid binary.
|
||||
LoadingMode::Unchecked => unsafe { Module::new_unchecked(&engine, code) },
|
||||
}
|
||||
.map_err(|err| {
|
||||
log::debug!(target: LOG_TARGET, "Module creation failed: {:?}", err);
|
||||
"Can't load the module into wasmi!"
|
||||
})?;
|
||||
|
||||
#[cfg(test)]
|
||||
tracker::LOADED_MODULE.with(|t| t.borrow_mut().push(loading_mode));
|
||||
|
||||
// Return a `LoadedModule` instance with
|
||||
// __valid__ module.
|
||||
Ok(LoadedModule { module, engine })
|
||||
}
|
||||
|
||||
/// Check that the module has required exported functions. For now
|
||||
/// these are just entrypoints:
|
||||
///
|
||||
/// - 'call'
|
||||
/// - 'deploy'
|
||||
///
|
||||
/// Any other exports are not allowed.
|
||||
fn scan_exports(&self) -> Result<(), &'static str> {
|
||||
let mut deploy_found = false;
|
||||
let mut call_found = false;
|
||||
let module = &self.module;
|
||||
let exports = module.exports();
|
||||
|
||||
for export in exports {
|
||||
match export.ty() {
|
||||
ExternType::Func(ft) => {
|
||||
match export.name() {
|
||||
"call" => call_found = true,
|
||||
"deploy" => deploy_found = true,
|
||||
_ =>
|
||||
return Err(
|
||||
"unknown function export: expecting only deploy and call functions",
|
||||
),
|
||||
}
|
||||
// Check the signature.
|
||||
// Both "call" and "deploy" have the () -> () function type.
|
||||
// We still support () -> (i32) for backwards compatibility.
|
||||
if !(ft.params().is_empty() &&
|
||||
(ft.results().is_empty() || ft.results() == [WasmiValueType::I32]))
|
||||
{
|
||||
return Err("entry point has wrong signature");
|
||||
}
|
||||
},
|
||||
ExternType::Memory(_) => return Err("memory export is forbidden"),
|
||||
ExternType::Global(_) => return Err("global export is forbidden"),
|
||||
ExternType::Table(_) => return Err("table export is forbidden"),
|
||||
}
|
||||
}
|
||||
|
||||
if !deploy_found {
|
||||
return Err("deploy function isn't exported");
|
||||
}
|
||||
if !call_found {
|
||||
return Err("call function isn't exported");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Scan an import section if any.
|
||||
///
|
||||
/// This makes sure that:
|
||||
/// - The import section looks as we expect it from a contract.
|
||||
/// - The limits of the memory type declared by the contract comply with the Schedule.
|
||||
///
|
||||
/// Returns the checked memory limits back to caller.
|
||||
///
|
||||
/// This method fails if:
|
||||
///
|
||||
/// - Memory import not found in the module.
|
||||
/// - Tables or globals found among imports.
|
||||
/// - `call_chain_extension` host function is imported, while chain extensions are disabled.
|
||||
///
|
||||
/// NOTE that only single memory instance is allowed for contract modules, which is enforced by
|
||||
/// this check combined with multi_memory proposal gets disabled in the engine.
|
||||
pub fn scan_imports<T: Config>(
|
||||
&self,
|
||||
schedule: &Schedule<T>,
|
||||
) -> Result<(u32, u32), &'static str> {
|
||||
let module = &self.module;
|
||||
let imports = module.imports();
|
||||
let mut memory_limits = None;
|
||||
|
||||
for import in imports {
|
||||
match *import.ty() {
|
||||
ExternType::Table(_) => return Err("Cannot import tables"),
|
||||
ExternType::Global(_) => return Err("Cannot import globals"),
|
||||
ExternType::Func(_) => {
|
||||
import.ty().func().ok_or("expected a function")?;
|
||||
|
||||
if !<T as Config>::ChainExtension::enabled() &&
|
||||
(import.name().as_bytes() == b"seal_call_chain_extension" ||
|
||||
import.name().as_bytes() == b"call_chain_extension")
|
||||
{
|
||||
return Err(
|
||||
"Module uses chain extensions but chain extensions are disabled",
|
||||
);
|
||||
}
|
||||
},
|
||||
ExternType::Memory(mt) => {
|
||||
if import.module().as_bytes() != IMPORT_MODULE_MEMORY.as_bytes() {
|
||||
return Err("Invalid module for imported memory");
|
||||
}
|
||||
if import.name().as_bytes() != b"memory" {
|
||||
return Err("Memory import must have the field name 'memory'");
|
||||
}
|
||||
if memory_limits.is_some() {
|
||||
return Err("Multiple memory imports defined");
|
||||
}
|
||||
// Parse memory limits defaulting it to (0,0).
|
||||
// Any access to it will then lead to out of bounds trap.
|
||||
let (initial, maximum) = (
|
||||
mt.initial_pages().to_bytes().unwrap_or(0).saturating_div(BYTES_PER_PAGE)
|
||||
as u32,
|
||||
mt.maximum_pages().map_or(schedule.limits.memory_pages, |p| {
|
||||
p.to_bytes().unwrap_or(0).saturating_div(BYTES_PER_PAGE) as u32
|
||||
}),
|
||||
);
|
||||
if initial > maximum {
|
||||
return Err(
|
||||
"Requested initial number of memory pages should not exceed the requested maximum",
|
||||
);
|
||||
}
|
||||
if maximum > schedule.limits.memory_pages {
|
||||
return Err("Maximum number of memory pages should not exceed the maximum configured in the Schedule");
|
||||
}
|
||||
|
||||
memory_limits = Some((initial, maximum));
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
memory_limits.ok_or("No memory import found in the module")
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that given `code` satisfies constraints required for the contract Wasm module.
|
||||
/// This includes two groups of checks:
|
||||
///
|
||||
/// 1. General engine-side validation makes sure the module is consistent and does not contain
|
||||
/// forbidden WebAssembly features.
|
||||
/// 2. Additional checks which are specific to smart contracts eligible for this pallet.
|
||||
fn validate<E, T>(
|
||||
code: &[u8],
|
||||
schedule: &Schedule<T>,
|
||||
determinism: &mut Determinism,
|
||||
) -> Result<(), (DispatchError, &'static str)>
|
||||
where
|
||||
E: Environment<()>,
|
||||
T: Config,
|
||||
{
|
||||
let module = (|| {
|
||||
// We don't actually ever execute this instance so we can get away with a minimal stack
|
||||
// which reduces the amount of memory that needs to be zeroed.
|
||||
let stack_limits = Some(StackLimits::new(1, 1, 0).expect("initial <= max; qed"));
|
||||
|
||||
// We check that the module is generally valid,
|
||||
// and does not have restricted WebAssembly features, here.
|
||||
let contract_module = match *determinism {
|
||||
Determinism::Relaxed => {
|
||||
if let Ok(module) = LoadedModule::new::<T>(
|
||||
code,
|
||||
Determinism::Enforced,
|
||||
stack_limits,
|
||||
LoadingMode::Checked,
|
||||
CompilationMode::Eager,
|
||||
) {
|
||||
*determinism = Determinism::Enforced;
|
||||
module
|
||||
} else {
|
||||
LoadedModule::new::<T>(
|
||||
code,
|
||||
Determinism::Relaxed,
|
||||
None,
|
||||
LoadingMode::Checked,
|
||||
CompilationMode::Eager,
|
||||
)?
|
||||
}
|
||||
},
|
||||
Determinism::Enforced => LoadedModule::new::<T>(
|
||||
code,
|
||||
Determinism::Enforced,
|
||||
stack_limits,
|
||||
LoadingMode::Checked,
|
||||
CompilationMode::Eager,
|
||||
)?,
|
||||
};
|
||||
|
||||
// The we check that module satisfies constraints the pallet puts on contracts.
|
||||
contract_module.scan_exports()?;
|
||||
contract_module.scan_imports::<T>(schedule)?;
|
||||
Ok(contract_module)
|
||||
})()
|
||||
.map_err(|msg: &str| {
|
||||
log::debug!(target: LOG_TARGET, "New code rejected on validation: {}", msg);
|
||||
(Error::<T>::CodeRejected.into(), msg)
|
||||
})?;
|
||||
|
||||
// This will make sure that the module can be actually run within wasmi:
|
||||
//
|
||||
// - It doesn't use any unknown imports.
|
||||
// - It doesn't explode the wasmi bytecode generation.
|
||||
WasmBlob::<T>::instantiate::<E, _>(module, (), schedule, AllowDeprecatedInterface::No)
|
||||
.map_err(|err| {
|
||||
log::debug!(target: LOG_TARGET, "{err}");
|
||||
(Error::<T>::CodeRejected.into(), "New code rejected on wasmi instantiation!")
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates the given binary `code` is a valid Wasm module satisfying following constraints:
|
||||
///
|
||||
/// - The module doesn't export any memory.
|
||||
/// - The module does imports memory, which limits lay within the limits permitted by the
|
||||
/// `schedule`.
|
||||
/// - All imported functions from the external environment match defined by `env` module.
|
||||
///
|
||||
/// Also constructs contract `code_info` by calculating the storage deposit.
|
||||
pub fn prepare<E, T>(
|
||||
code: CodeVec<T>,
|
||||
schedule: &Schedule<T>,
|
||||
owner: AccountIdOf<T>,
|
||||
mut determinism: Determinism,
|
||||
) -> Result<WasmBlob<T>, (DispatchError, &'static str)>
|
||||
where
|
||||
E: Environment<()>,
|
||||
T: Config,
|
||||
{
|
||||
validate::<E, T>(code.as_ref(), schedule, &mut determinism)?;
|
||||
|
||||
// Calculate deposit for storing contract code and `code_info` in two different storage items.
|
||||
let code_len = code.len() as u32;
|
||||
let bytes_added = code_len.saturating_add(<CodeInfo<T>>::max_encoded_len() as u32);
|
||||
let deposit = Diff { bytes_added, items_added: 2, ..Default::default() }
|
||||
.update_contract::<T>(None)
|
||||
.charge_or_zero();
|
||||
let code_info = CodeInfo { owner, deposit, determinism, refcount: 0, code_len };
|
||||
let code_hash = T::Hashing::hash(&code);
|
||||
|
||||
Ok(WasmBlob { code, code_info, code_hash })
|
||||
}
|
||||
|
||||
/// Alternate (possibly unsafe) preparation functions used only for benchmarking and testing.
|
||||
///
|
||||
/// For benchmarking we need to construct special contracts that might not pass our
|
||||
/// sanity checks. We hide functions allowing this behind a feature that is only set during
|
||||
/// benchmarking or testing to prevent usage in production code.
|
||||
#[cfg(any(test, feature = "runtime-benchmarks"))]
|
||||
pub mod benchmarking {
|
||||
use super::*;
|
||||
|
||||
/// Prepare function that does not perform export section checks on the passed in code.
|
||||
pub fn prepare<T: Config>(
|
||||
code: Vec<u8>,
|
||||
schedule: &Schedule<T>,
|
||||
owner: AccountIdOf<T>,
|
||||
) -> Result<WasmBlob<T>, DispatchError> {
|
||||
let determinism = Determinism::Enforced;
|
||||
let contract_module = LoadedModule::new::<T>(
|
||||
&code,
|
||||
determinism,
|
||||
None,
|
||||
LoadingMode::Checked,
|
||||
CompilationMode::Eager,
|
||||
)?;
|
||||
contract_module.scan_imports::<T>(schedule)?;
|
||||
let code: CodeVec<T> = code.try_into().map_err(|_| <Error<T>>::CodeTooLarge)?;
|
||||
let code_info = CodeInfo {
|
||||
owner,
|
||||
// this is a helper function for benchmarking which skips deposit collection
|
||||
deposit: Default::default(),
|
||||
refcount: 0,
|
||||
code_len: code.len() as u32,
|
||||
determinism,
|
||||
};
|
||||
let code_hash = T::Hashing::hash(&code);
|
||||
|
||||
Ok(WasmBlob { code, code_info, code_hash })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
exec::Ext,
|
||||
schedule::Limits,
|
||||
tests::{Test, ALICE},
|
||||
};
|
||||
use pezpallet_contracts_proc_macro::define_env;
|
||||
use std::fmt;
|
||||
|
||||
impl fmt::Debug for WasmBlob<Test> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "ContractCode {{ .. }}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Using unreachable statements triggers unreachable warnings in the generated code
|
||||
#[allow(unreachable_code)]
|
||||
mod env {
|
||||
use super::*;
|
||||
use crate::wasm::runtime::{AllowDeprecatedInterface, AllowUnstableInterface, TrapReason};
|
||||
|
||||
// Define test environment for tests. We need ImportSatisfyCheck
|
||||
// implementation from it. So actual implementations doesn't matter.
|
||||
#[define_env]
|
||||
pub mod test_env {
|
||||
fn panic(_ctx: _, _memory: _) -> Result<(), TrapReason> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// gas is an implementation defined function and a contract can't import it.
|
||||
fn gas(_ctx: _, _memory: _, _amount: u64) -> Result<(), TrapReason> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn nop(_ctx: _, _memory: _, _unused: u64) -> Result<(), TrapReason> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// new version of nop with other data type for argument
|
||||
#[version(1)]
|
||||
fn nop(_ctx: _, _memory: _, _unused: i32) -> Result<(), TrapReason> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! prepare_test {
|
||||
($name:ident, $wat:expr, $($expected:tt)*) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let wasm = wat::parse_str($wat).unwrap().try_into().unwrap();
|
||||
let schedule = Schedule {
|
||||
limits: Limits {
|
||||
memory_pages: 16,
|
||||
.. Default::default()
|
||||
},
|
||||
.. Default::default()
|
||||
};
|
||||
let r = prepare::<env::Env, Test>(
|
||||
wasm,
|
||||
&schedule,
|
||||
ALICE,
|
||||
Determinism::Enforced,
|
||||
);
|
||||
assert_matches::assert_matches!(r.map_err(|(_, msg)| msg), $($expected)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
prepare_test!(
|
||||
no_floats,
|
||||
r#"
|
||||
(module
|
||||
(func (export "call")
|
||||
(drop
|
||||
(f32.add
|
||||
(f32.const 0)
|
||||
(f32.const 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
(func (export "deploy"))
|
||||
)"#,
|
||||
Err("Can't load the module into wasmi!")
|
||||
);
|
||||
|
||||
mod memories {
|
||||
use super::*;
|
||||
|
||||
prepare_test!(
|
||||
memory_with_one_page,
|
||||
r#"
|
||||
(module
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Ok(_)
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
internal_memory_declaration,
|
||||
r#"
|
||||
(module
|
||||
(memory 1 1)
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("No memory import found in the module")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
no_memory_import,
|
||||
r#"
|
||||
(module
|
||||
;; no memory imported
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)"#,
|
||||
Err("No memory import found in the module")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
initial_exceeds_maximum,
|
||||
r#"
|
||||
(module
|
||||
(import "env" "memory" (memory 16 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Can't load the module into wasmi!")
|
||||
);
|
||||
|
||||
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
|
||||
(import "env" "memory" (memory 1 17))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Maximum number of memory pages should not exceed the maximum configured in the Schedule")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
field_name_not_memory,
|
||||
r#"
|
||||
(module
|
||||
(import "env" "forgetit" (memory 1 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Memory import must have the field name 'memory'")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
multiple_memory_imports,
|
||||
r#"
|
||||
(module
|
||||
(import "env" "memory" (memory 1 1))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Can't load the module into wasmi!")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
table_import,
|
||||
r#"
|
||||
(module
|
||||
(import "seal0" "table" (table 1 anyfunc))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Cannot import tables")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
global_import,
|
||||
r#"
|
||||
(module
|
||||
(global $g (import "seal0" "global") i32)
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Cannot import globals")
|
||||
);
|
||||
}
|
||||
|
||||
mod imports {
|
||||
use super::*;
|
||||
|
||||
prepare_test!(
|
||||
can_import_legit_function,
|
||||
r#"
|
||||
(module
|
||||
(import "seal0" "nop" (func (param i64)))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Ok(_)
|
||||
);
|
||||
|
||||
// memory is in "env" and not in "seal0"
|
||||
prepare_test!(
|
||||
memory_not_in_seal0,
|
||||
r#"
|
||||
(module
|
||||
(import "seal0" "memory" (memory 1 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Invalid module for imported memory")
|
||||
);
|
||||
|
||||
// Memory is in "env" and not in some arbitrary module
|
||||
prepare_test!(
|
||||
memory_not_in_arbitrary_module,
|
||||
r#"
|
||||
(module
|
||||
(import "any_module" "memory" (memory 1 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Invalid module for imported memory")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
function_in_other_module_works,
|
||||
r#"
|
||||
(module
|
||||
(import "seal1" "nop" (func (param i32)))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Ok(_)
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
wrong_signature,
|
||||
r#"
|
||||
(module
|
||||
(import "seal0" "input" (func (param i64)))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("New code rejected on wasmi instantiation!")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
unknown_func_name,
|
||||
r#"
|
||||
(module
|
||||
(import "seal0" "unknown_func" (func))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("No memory import found in the module")
|
||||
);
|
||||
|
||||
// Try to import function from not a "seal*" module.
|
||||
prepare_test!(
|
||||
try_import_from_wrong_module,
|
||||
r#"
|
||||
(module
|
||||
(import "env" "panic" (func))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("New code rejected on wasmi instantiation!")
|
||||
);
|
||||
}
|
||||
|
||||
mod entrypoints {
|
||||
use super::*;
|
||||
|
||||
prepare_test!(
|
||||
it_works,
|
||||
r#"
|
||||
(module
|
||||
(import "env" "memory" (memory 1 1))
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Ok(_)
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
signed_extension_works,
|
||||
r#"
|
||||
(module
|
||||
(import "env" "memory" (memory 1 1))
|
||||
(func (export "deploy"))
|
||||
(func (export "call"))
|
||||
(func (param i32) (result i32)
|
||||
local.get 0
|
||||
i32.extend8_s
|
||||
)
|
||||
)
|
||||
"#,
|
||||
Ok(_)
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
omit_memory,
|
||||
r#"
|
||||
(module
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("No memory import found in the module")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
omit_deploy,
|
||||
r#"
|
||||
(module
|
||||
(func (export "call"))
|
||||
)
|
||||
"#,
|
||||
Err("deploy function isn't exported")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
omit_call,
|
||||
r#"
|
||||
(module
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("call function isn't exported")
|
||||
);
|
||||
|
||||
// Try to use imported function as an entry point.
|
||||
// This is allowed.
|
||||
prepare_test!(
|
||||
try_sneak_export_as_entrypoint,
|
||||
r#"
|
||||
(module
|
||||
(import "seal0" "panic" (func))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
(func (export "deploy"))
|
||||
|
||||
(export "call" (func 0))
|
||||
)
|
||||
"#,
|
||||
Ok(_)
|
||||
);
|
||||
|
||||
// Try to use global as an entry point.
|
||||
prepare_test!(
|
||||
try_sneak_export_as_global,
|
||||
r#"
|
||||
(module
|
||||
(func (export "deploy"))
|
||||
(global (export "call") i32 (i32.const 0))
|
||||
)
|
||||
"#,
|
||||
Err("global export is forbidden")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
wrong_signature,
|
||||
r#"
|
||||
(module
|
||||
(func (export "deploy"))
|
||||
(func (export "call") (param i32))
|
||||
)
|
||||
"#,
|
||||
Err("entry point has wrong signature")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
unknown_exports,
|
||||
r#"
|
||||
(module
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
(func (export "whatevs"))
|
||||
)
|
||||
"#,
|
||||
Err("unknown function export: expecting only deploy and call functions")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
global_float,
|
||||
r#"
|
||||
(module
|
||||
(global $x f32 (f32.const 0))
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Can't load the module into wasmi!")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
local_float,
|
||||
r#"
|
||||
(module
|
||||
(func $foo (local f32))
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Can't load the module into wasmi!")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
param_float,
|
||||
r#"
|
||||
(module
|
||||
(func $foo (param f32))
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Can't load the module into wasmi!")
|
||||
);
|
||||
|
||||
prepare_test!(
|
||||
result_float,
|
||||
r#"
|
||||
(module
|
||||
(func $foo (result f32) (f32.const 0))
|
||||
(func (export "call"))
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#,
|
||||
Err("Can't load the module into wasmi!")
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user