contracts: Allow indeterministic instructions off-chain (#12469)

* Allow indetermistic instructions off-chain

* Apply suggestions from code review

Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>

* fmt

Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>
This commit is contained in:
Alexander Theißen
2022-10-24 19:48:04 +02:00
committed by GitHub
parent d0dcf008ec
commit 3ae4be8662
15 changed files with 926 additions and 169 deletions
@@ -24,7 +24,7 @@
//! 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 crate::{Config, Determinism};
use frame_support::traits::Get;
use sp_core::crypto::UncheckedFrom;
use sp_runtime::traits::Hash;
@@ -554,7 +554,7 @@ where
fn inject_gas_metering<T: Config>(module: Module) -> Module {
let schedule = T::Schedule::get();
let gas_rules = schedule.rules(&module);
let gas_rules = schedule.rules(&module, Determinism::Deterministic);
wasm_instrument::gas_metering::inject(module, &gas_rules, "seal0").unwrap()
}
@@ -371,7 +371,7 @@ benchmarks! {
T::Currency::make_free_balance_be(&caller, caller_funding::<T>());
let WasmModule { code, hash, .. } = WasmModule::<T>::sized(c, Location::Call);
let origin = RawOrigin::Signed(caller.clone());
}: _(origin, code, None)
}: _(origin, code, None, Determinism::Deterministic)
verify {
// uploading the code reserves some balance in the callers account
assert!(T::Currency::reserved_balance(&caller) > 0u32.into());
@@ -386,7 +386,7 @@ benchmarks! {
T::Currency::make_free_balance_be(&caller, caller_funding::<T>());
let WasmModule { code, hash, .. } = WasmModule::<T>::dummy();
let origin = RawOrigin::Signed(caller.clone());
let uploaded = <Contracts<T>>::bare_upload_code(caller.clone(), code, None)?;
let uploaded = <Contracts<T>>::bare_upload_code(caller.clone(), code, None, Determinism::Deterministic)?;
assert_eq!(uploaded.code_hash, hash);
assert_eq!(uploaded.deposit, T::Currency::reserved_balance(&caller));
assert!(<Contract<T>>::code_exists(&hash));
@@ -2894,6 +2894,7 @@ benchmarks! {
None,
data,
false,
Determinism::Deterministic,
)
.result?;
}
@@ -2941,6 +2942,7 @@ benchmarks! {
None,
data,
false,
Determinism::Deterministic,
)
.result?;
}
+74 -8
View File
@@ -18,7 +18,7 @@
use crate::{
gas::GasMeter,
storage::{self, Storage, WriteOutcome},
BalanceOf, CodeHash, Config, ContractInfo, ContractInfoOf, Error, Event, Nonce,
BalanceOf, CodeHash, Config, ContractInfo, ContractInfoOf, Determinism, Error, Event, Nonce,
Pallet as Contracts, Schedule,
};
use frame_support::{
@@ -355,6 +355,9 @@ pub trait Executable<T: Config>: Sized {
/// Size of the instrumented code in bytes.
fn code_len(&self) -> u32;
/// The code does not contain any instructions which could lead to indeterminism.
fn is_deterministic(&self) -> bool;
}
/// The complete call stack of a contract execution.
@@ -395,6 +398,8 @@ pub struct Stack<'a, T: Config, E> {
/// All the bytes added to this field should be valid UTF-8. The buffer has no defined
/// structure and is intended to be shown to users as-is for debugging purposes.
debug_message: Option<&'a mut Vec<u8>>,
/// The determinism requirement of this call stack.
determinism: Determinism,
/// No executable is held by the struct but influences its behaviour.
_phantom: PhantomData<E>,
}
@@ -601,6 +606,7 @@ where
value: BalanceOf<T>,
input_data: Vec<u8>,
debug_message: Option<&'a mut Vec<u8>>,
determinism: Determinism,
) -> Result<ExecReturnValue, ExecError> {
let (mut stack, executable) = Self::new(
FrameArgs::Call { dest, cached_info: None, delegated_call: None },
@@ -610,6 +616,7 @@ where
schedule,
value,
debug_message,
determinism,
)?;
stack.run(executable, input_data)
}
@@ -648,6 +655,7 @@ where
schedule,
value,
debug_message,
Determinism::Deterministic,
)?;
let account_id = stack.top_frame().account_id.clone();
stack.run(executable, input_data).map(|ret| (account_id, ret))
@@ -662,9 +670,17 @@ where
schedule: &'a Schedule<T>,
value: BalanceOf<T>,
debug_message: Option<&'a mut Vec<u8>>,
determinism: Determinism,
) -> Result<(Self, E), ExecError> {
let (first_frame, executable, nonce) =
Self::new_frame(args, value, gas_meter, storage_meter, Weight::zero(), schedule)?;
let (first_frame, executable, nonce) = Self::new_frame(
args,
value,
gas_meter,
storage_meter,
Weight::zero(),
schedule,
determinism,
)?;
let stack = Self {
origin,
schedule,
@@ -676,6 +692,7 @@ where
first_frame,
frames: Default::default(),
debug_message,
determinism,
_phantom: Default::default(),
};
@@ -693,6 +710,7 @@ where
storage_meter: &mut storage::meter::GenericMeter<T, S>,
gas_limit: Weight,
schedule: &Schedule<T>,
determinism: Determinism,
) -> Result<(Frame<T>, E, Option<u64>), ExecError> {
let (account_id, contract_info, executable, delegate_caller, entry_point, nonce) =
match frame_args {
@@ -729,6 +747,15 @@ where
},
};
// `AllowIndeterminism` will only be ever set in case of off-chain execution.
// Instantiations are never allowed even when executing off-chain.
if !(executable.is_deterministic() ||
(matches!(determinism, Determinism::AllowIndeterminism) &&
matches!(entry_point, ExportedFunction::Call)))
{
return Err(Error::<T>::Indeterministic.into())
}
let frame = Frame {
delegate_caller,
value_transferred,
@@ -775,6 +802,7 @@ where
nested_storage,
gas_limit,
self.schedule,
self.determinism,
)?;
self.frames.push(frame);
Ok(executable)
@@ -1328,15 +1356,18 @@ where
}
fn set_code_hash(&mut self, hash: CodeHash<Self::T>) -> Result<(), DispatchError> {
let frame = top_frame_mut!(self);
if !E::from_storage(hash, self.schedule, &mut frame.nested_gas)?.is_deterministic() {
return Err(<Error<T>>::Indeterministic.into())
}
E::add_user(hash)?;
let top_frame = self.top_frame_mut();
let prev_hash = top_frame.contract_info().code_hash;
let prev_hash = frame.contract_info().code_hash;
E::remove_user(prev_hash);
top_frame.contract_info().code_hash = hash;
frame.contract_info().code_hash = hash;
Contracts::<Self::T>::deposit_event(
vec![T::Hashing::hash_of(&top_frame.account_id), hash, prev_hash],
vec![T::Hashing::hash_of(&frame.account_id), hash, prev_hash],
Event::ContractCodeUpdated {
contract: top_frame.account_id.clone(),
contract: frame.account_id.clone(),
new_code_hash: hash,
old_code_hash: prev_hash,
},
@@ -1513,6 +1544,10 @@ mod tests {
fn code_len(&self) -> u32 {
0
}
fn is_deterministic(&self) -> bool {
true
}
}
fn exec_success() -> ExecResult {
@@ -1551,6 +1586,7 @@ mod tests {
value,
vec![],
None,
Determinism::Deterministic,
),
Ok(_)
);
@@ -1604,6 +1640,7 @@ mod tests {
value,
vec![],
None,
Determinism::Deterministic,
)
.unwrap();
@@ -1645,6 +1682,7 @@ mod tests {
value,
vec![],
None,
Determinism::Deterministic,
)
.unwrap();
@@ -1680,6 +1718,7 @@ mod tests {
55,
vec![],
None,
Determinism::Deterministic,
)
.unwrap();
@@ -1731,6 +1770,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
);
let output = result.unwrap();
@@ -1763,6 +1803,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
);
let output = result.unwrap();
@@ -1793,6 +1834,7 @@ mod tests {
0,
vec![1, 2, 3, 4],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
});
@@ -1873,6 +1915,7 @@ mod tests {
value,
vec![],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
@@ -1918,6 +1961,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
@@ -1951,6 +1995,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
});
@@ -1980,6 +2025,7 @@ mod tests {
0,
vec![0],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
});
@@ -2007,6 +2053,7 @@ mod tests {
0,
vec![0],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
});
@@ -2042,6 +2089,7 @@ mod tests {
0,
vec![0],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
});
@@ -2077,6 +2125,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
@@ -2235,6 +2284,7 @@ mod tests {
min_balance * 10,
vec![],
None,
Determinism::Deterministic,
),
Ok(_)
);
@@ -2299,6 +2349,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
),
Ok(_)
);
@@ -2384,6 +2435,7 @@ mod tests {
0,
vec![0],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
});
@@ -2450,6 +2502,7 @@ mod tests {
0,
vec![],
Some(&mut debug_buffer),
Determinism::Deterministic,
)
.unwrap();
});
@@ -2483,6 +2536,7 @@ mod tests {
0,
vec![],
Some(&mut debug_buffer),
Determinism::Deterministic,
);
assert!(result.is_err());
});
@@ -2516,6 +2570,7 @@ mod tests {
0,
CHARLIE.encode(),
None,
Determinism::Deterministic
));
// Calling into oneself fails
@@ -2529,6 +2584,7 @@ mod tests {
0,
BOB.encode(),
None,
Determinism::Deterministic
)
.map_err(|e| e.error),
<Error<Test>>::ReentranceDenied,
@@ -2567,6 +2623,7 @@ mod tests {
0,
vec![0],
None,
Determinism::Deterministic
)
.map_err(|e| e.error),
<Error<Test>>::ReentranceDenied,
@@ -2601,6 +2658,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
)
.unwrap();
@@ -2683,6 +2741,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
)
.unwrap();
@@ -2886,6 +2945,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic
));
});
}
@@ -3012,6 +3072,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic
));
});
}
@@ -3047,6 +3108,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic
));
});
}
@@ -3082,6 +3144,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic
));
});
}
@@ -3143,6 +3206,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic
));
});
}
@@ -3204,6 +3268,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic
));
});
}
@@ -3235,6 +3300,7 @@ mod tests {
0,
vec![],
None,
Determinism::Deterministic,
);
assert_matches!(result, Ok(_));
});
+30 -9
View File
@@ -132,6 +132,7 @@ pub use crate::{
migration::Migration,
pallet::*,
schedule::{HostFnWeights, InstructionWeights, Limits, Schedule},
wasm::Determinism,
};
type CodeHash<T> = <T as frame_system::Config>::Hash;
@@ -206,7 +207,7 @@ pub mod pallet {
use frame_system::pallet_prelude::*;
/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(8);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(9);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
@@ -456,6 +457,10 @@ pub mod pallet {
/// the in storage version to the current
/// [`InstructionWeights::version`](InstructionWeights).
///
/// - `determinism`: If this is set to any other value but [`Determinism::Deterministic`]
/// then the only way to use this code is to delegate call into it from an offchain
/// execution. Set to [`Determinism::Deterministic`] if in doubt.
///
/// # Note
///
/// Anyone can instantiate a contract from any uploaded code and thus prevent its removal.
@@ -467,9 +472,11 @@ pub mod pallet {
origin: OriginFor<T>,
code: Vec<u8>,
storage_deposit_limit: Option<<BalanceOf<T> as codec::HasCompact>::Type>,
determinism: Determinism,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
Self::bare_upload_code(origin, code, storage_deposit_limit.map(Into::into)).map(|_| ())
Self::bare_upload_code(origin, code, storage_deposit_limit.map(Into::into), determinism)
.map(|_| ())
}
/// Remove the code stored under `code_hash` and refund the deposit to its owner.
@@ -562,6 +569,7 @@ pub mod pallet {
storage_deposit_limit.map(Into::into),
data,
None,
Determinism::Deterministic,
);
if let Ok(retval) = &output.result {
if retval.did_revert() {
@@ -825,6 +833,8 @@ pub mod pallet {
/// A more detailed error can be found on the node console if debug messages are enabled
/// or in the debug buffer which is returned to RPC clients.
CodeRejected,
/// An indetermistic code was used in a context where this is not permitted.
Indeterministic,
}
/// A mapping from an original code hash to the original code, untouched by instrumentation.
@@ -921,6 +931,7 @@ where
storage_deposit_limit: Option<BalanceOf<T>>,
data: Vec<u8>,
debug: bool,
determinism: Determinism,
) -> ContractExecResult<BalanceOf<T>> {
let mut debug_message = if debug { Some(Vec::new()) } else { None };
let output = Self::internal_call(
@@ -931,6 +942,7 @@ where
storage_deposit_limit,
data,
debug_message.as_mut(),
determinism,
);
ContractExecResult {
result: output.result.map_err(|r| r.error),
@@ -994,10 +1006,11 @@ where
origin: T::AccountId,
code: Vec<u8>,
storage_deposit_limit: Option<BalanceOf<T>>,
determinism: Determinism,
) -> CodeUploadResult<CodeHash<T>, BalanceOf<T>> {
let schedule = T::Schedule::get();
let module =
PrefabWasmModule::from_code(code, &schedule, origin).map_err(|(err, _)| err)?;
let module = PrefabWasmModule::from_code(code, &schedule, origin, determinism)
.map_err(|(err, _)| err)?;
let deposit = module.open_deposit();
if let Some(storage_deposit_limit) = storage_deposit_limit {
ensure!(storage_deposit_limit >= deposit, <Error<T>>::StorageDepositLimitExhausted);
@@ -1067,6 +1080,7 @@ where
storage_deposit_limit: Option<BalanceOf<T>>,
data: Vec<u8>,
debug_message: Option<&mut Vec<u8>>,
determinism: Determinism,
) -> InternalCallOutput<T> {
let mut gas_meter = GasMeter::new(gas_limit);
let mut storage_meter = match StorageMeter::new(&origin, storage_deposit_limit, value) {
@@ -1088,6 +1102,7 @@ where
value,
data,
debug_message,
determinism,
);
InternalCallOutput {
result,
@@ -1115,11 +1130,16 @@ where
let schedule = T::Schedule::get();
let (extra_deposit, executable) = match code {
Code::Upload(binary) => {
let executable = PrefabWasmModule::from_code(binary, &schedule, origin.clone())
.map_err(|(err, msg)| {
debug_message.as_mut().map(|buffer| buffer.extend(msg.as_bytes()));
err
})?;
let executable = PrefabWasmModule::from_code(
binary,
&schedule,
origin.clone(),
Determinism::Deterministic,
)
.map_err(|(err, msg)| {
debug_message.as_mut().map(|buffer| buffer.extend(msg.as_bytes()));
err
})?;
// The open deposit will be charged during execution when the
// uploaded module does not already exist. This deposit is not part of the
// storage meter because it is not transferred to the contract but
@@ -1218,6 +1238,7 @@ sp_api::decl_runtime_apis! {
origin: AccountId,
code: Vec<u8>,
storage_deposit_limit: Option<Balance>,
determinism: Determinism,
) -> CodeUploadResult<Hash, Balance>;
/// Query a given storage key in a given contract.
+127 -70
View File
@@ -32,65 +32,54 @@ use sp_std::{marker::PhantomData, prelude::*};
pub struct Migration<T: Config>(PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for Migration<T> {
fn on_runtime_upgrade() -> Weight {
let version = StorageVersion::get::<Pallet<T>>();
let version = <Pallet<T>>::on_chain_storage_version();
let mut weight = Weight::zero();
if version < 4 {
weight = weight.saturating_add(v4::migrate::<T>());
StorageVersion::new(4).put::<Pallet<T>>();
v4::migrate::<T>(&mut weight);
}
if version < 5 {
weight = weight.saturating_add(v5::migrate::<T>());
StorageVersion::new(5).put::<Pallet<T>>();
v5::migrate::<T>(&mut weight);
}
if version < 6 {
weight = weight.saturating_add(v6::migrate::<T>());
StorageVersion::new(6).put::<Pallet<T>>();
v6::migrate::<T>(&mut weight);
}
if version < 7 {
weight = weight.saturating_add(v7::migrate::<T>());
StorageVersion::new(7).put::<Pallet<T>>();
v7::migrate::<T>(&mut weight);
}
if version < 8 {
weight = weight.saturating_add(v8::migrate::<T>());
StorageVersion::new(8).put::<Pallet<T>>();
v8::migrate::<T>(&mut weight);
}
if version < 9 {
v9::migrate::<T>(&mut weight);
}
StorageVersion::new(9).put::<Pallet<T>>();
weight.saturating_accrue(T::DbWeight::get().writes(1));
weight
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, &'static str> {
let version = StorageVersion::get::<Pallet<T>>();
let version = <Pallet<T>>::on_chain_storage_version();
if version < 7 {
return Ok(vec![])
}
if version < 8 {
if version == 8 {
v8::pre_upgrade::<T>()?;
}
Ok(vec![])
Ok(version.encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), &'static str> {
let version = StorageVersion::get::<Pallet<T>>();
if version < 7 {
return Ok(())
}
if version < 8 {
v8::post_upgrade::<T>()?;
}
Ok(())
fn post_upgrade(state: Vec<u8>) -> Result<(), &'static str> {
let version = Decode::decode(&mut state.as_ref()).map_err(|_| "Cannot decode version")?;
post_checks::post_upgrade::<T>(version)
}
}
@@ -98,10 +87,10 @@ impl<T: Config> OnRuntimeUpgrade for Migration<T> {
mod v4 {
use super::*;
pub fn migrate<T: Config>() -> Weight {
pub fn migrate<T: Config>(weight: &mut Weight) {
#[allow(deprecated)]
migration::remove_storage_prefix(<Pallet<T>>::name().as_bytes(), b"CurrentSchedule", b"");
T::DbWeight::get().writes(1)
weight.saturating_accrue(T::DbWeight::get().writes(1));
}
}
@@ -169,11 +158,9 @@ mod v5 {
#[storage_alias]
type DeletionQueue<T: Config> = StorageValue<Pallet<T>, Vec<DeletedContract>>;
pub fn migrate<T: Config>() -> Weight {
let mut weight = Weight::zero();
pub fn migrate<T: Config>(weight: &mut Weight) {
<ContractInfoOf<T>>::translate(|_key, old: OldContractInfo<T>| {
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
match old {
OldContractInfo::Alive(old) => Some(ContractInfo::<T> {
trie_id: old.trie_id,
@@ -185,12 +172,10 @@ mod v5 {
});
DeletionQueue::<T>::translate(|old: Option<Vec<OldDeletedContract>>| {
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
old.map(|old| old.into_iter().map(|o| DeletedContract { trie_id: o.trie_id }).collect())
})
.ok();
weight
}
}
@@ -214,14 +199,14 @@ mod v6 {
}
#[derive(Encode, Decode)]
struct PrefabWasmModule {
pub struct PrefabWasmModule {
#[codec(compact)]
instruction_weights_version: u32,
pub instruction_weights_version: u32,
#[codec(compact)]
initial: u32,
pub initial: u32,
#[codec(compact)]
maximum: u32,
code: Vec<u8>,
pub maximum: u32,
pub code: Vec<u8>,
}
use v5::ContractInfo as OldContractInfo;
@@ -258,11 +243,9 @@ mod v6 {
#[storage_alias]
type OwnerInfoOf<T: Config> = StorageMap<Pallet<T>, Identity, CodeHash<T>, OwnerInfo<T>>;
pub fn migrate<T: Config>() -> Weight {
let mut weight = Weight::zero();
pub fn migrate<T: Config>(weight: &mut Weight) {
<ContractInfoOf<T>>::translate(|_key, old: OldContractInfo<T>| {
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
Some(ContractInfo::<T> {
trie_id: old.trie_id,
code_hash: old.code_hash,
@@ -274,7 +257,7 @@ mod v6 {
.expect("Infinite input; no dead input space; qed");
<CodeStorage<T>>::translate(|key, old: OldPrefabWasmModule| {
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 2));
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2));
<OwnerInfoOf<T>>::insert(
key,
OwnerInfo {
@@ -290,8 +273,6 @@ mod v6 {
code: old.code,
})
});
weight
}
}
@@ -299,14 +280,14 @@ mod v6 {
mod v7 {
use super::*;
pub fn migrate<T: Config>() -> Weight {
pub fn migrate<T: Config>(weight: &mut Weight) {
#[storage_alias]
type AccountCounter<T: Config> = StorageValue<Pallet<T>, u64, ValueQuery>;
#[storage_alias]
type Nonce<T: Config> = StorageValue<Pallet<T>, u64, ValueQuery>;
Nonce::<T>::set(AccountCounter::<T>::take());
T::DbWeight::get().reads_writes(1, 2)
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2))
}
}
@@ -317,23 +298,21 @@ mod v8 {
use v6::ContractInfo as OldContractInfo;
#[derive(Encode, Decode)]
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>,
storage_base_deposit: BalanceOf<T>,
pub 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>,
}
#[storage_alias]
type ContractInfoOf<T: Config, V> =
StorageMap<Pallet<T>, Twox64Concat, <T as frame_system::Config>::AccountId, V>;
pub fn migrate<T: Config>() -> Weight {
let mut weight = Weight::zero();
pub fn migrate<T: Config>(weight: &mut Weight) {
<ContractInfoOf<T, ContractInfo<T>>>::translate_values(|old: OldContractInfo<T>| {
// Count storage items of this contract
let mut storage_bytes = 0u32;
@@ -359,8 +338,9 @@ mod v8 {
// Reads: One read for each storage item plus the contract info itself.
// Writes: Only the new contract info.
weight = weight
.saturating_add(T::DbWeight::get().reads_writes(u64::from(storage_items) + 1, 1));
weight.saturating_accrue(
T::DbWeight::get().reads_writes(u64::from(storage_items) + 1, 1),
);
Some(ContractInfo {
trie_id: old.trie_id,
@@ -372,8 +352,6 @@ mod v8 {
storage_base_deposit,
})
});
weight
}
#[cfg(feature = "try-runtime")]
@@ -385,9 +363,78 @@ mod v8 {
}
Ok(())
}
}
#[cfg(feature = "try-runtime")]
pub fn post_upgrade<T: Config>() -> Result<(), &'static str> {
/// Update `CodeStorage` with the new `determinism` field.
mod v9 {
use super::*;
use crate::Determinism;
use v6::PrefabWasmModule as OldPrefabWasmModule;
#[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>,
pub determinism: Determinism,
}
#[storage_alias]
type CodeStorage<T: Config> = StorageMap<Pallet<T>, Identity, CodeHash<T>, PrefabWasmModule>;
pub fn migrate<T: Config>(weight: &mut Weight) {
<CodeStorage<T>>::translate_values(|old: OldPrefabWasmModule| {
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
Some(PrefabWasmModule {
instruction_weights_version: old.instruction_weights_version,
initial: old.initial,
maximum: old.maximum,
code: old.code,
determinism: Determinism::Deterministic,
})
});
}
}
// Post checks always need to be run against the latest storage version. This is why we
// do not scope them in the per version modules. They always need to be ported to the latest
// version.
#[cfg(feature = "try-runtime")]
mod post_checks {
use super::*;
use crate::Determinism;
use sp_io::default_child_storage as child;
use v8::ContractInfo;
use v9::PrefabWasmModule;
#[storage_alias]
type CodeStorage<T: Config> = StorageMap<Pallet<T>, Identity, CodeHash<T>, PrefabWasmModule>;
#[storage_alias]
type ContractInfoOf<T: Config, V> =
StorageMap<Pallet<T>, Twox64Concat, <T as frame_system::Config>::AccountId, V>;
pub fn post_upgrade<T: Config>(old_version: StorageVersion) -> Result<(), &'static str> {
if old_version < 7 {
return Ok(())
}
if old_version < 8 {
v8::<T>()?;
}
if old_version < 9 {
v9::<T>()?;
}
Ok(())
}
fn v8<T: Config>() -> Result<(), &'static str> {
use frame_support::traits::ReservableCurrency;
for (key, value) in ContractInfoOf::<T, ContractInfo<T>>::iter() {
let reserved = T::Currency::reserved_balance(&key);
@@ -413,4 +460,14 @@ mod v8 {
}
Ok(())
}
fn v9<T: Config>() -> Result<(), &'static str> {
for value in CodeStorage::<T>::iter_values() {
ensure!(
value.determinism == Determinism::Deterministic,
"All pre-existing codes need to be deterministic."
);
}
Ok(())
}
}
+20 -3
View File
@@ -18,7 +18,7 @@
//! 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 crate::{wasm::Determinism, weights::WeightInfo, Config};
use codec::{Decode, Encode};
use frame_support::DefaultNoBound;
@@ -193,6 +193,13 @@ pub struct InstructionWeights<T: Config> {
/// Changes to other parts of the schedule should not increment the version in
/// order to avoid unnecessary re-instrumentations.
pub version: u32,
/// Weight to be used for instructions which don't have benchmarks assigned.
///
/// This weight is used whenever a code is uploaded with [`Determinism::AllowIndeterminism`]
/// and an instruction (usually a float instruction) is encountered. This weight is **not**
/// used if a contract is uploaded with [`Determinism::Deterministic`]. If this field is set to
/// `0` (the default) only deterministic codes are allowed to be uploaded.
pub fallback: u32,
pub i64const: u32,
pub i64load: u32,
pub i64store: u32,
@@ -526,6 +533,7 @@ impl<T: Config> Default for InstructionWeights<T> {
let max_pages = Limits::default().memory_pages;
Self {
version: 3,
fallback: 0,
i64const: cost_instr!(instr_i64const, 1),
i64load: cost_instr!(instr_i64load, 2),
i64store: cost_instr!(instr_i64store, 2),
@@ -659,10 +667,15 @@ impl<T: Config> Default for HostFnWeights<T> {
struct ScheduleRules<'a, T: Config> {
schedule: &'a Schedule<T>,
params: Vec<u32>,
determinism: Determinism,
}
impl<T: Config> Schedule<T> {
pub(crate) fn rules(&self, module: &elements::Module) -> impl gas_metering::Rules + '_ {
pub(crate) fn rules(
&self,
module: &elements::Module,
determinism: Determinism,
) -> impl gas_metering::Rules + '_ {
ScheduleRules {
schedule: self,
params: module
@@ -674,6 +687,7 @@ impl<T: Config> Schedule<T> {
func.params().len() as u32
})
.collect(),
determinism,
}
}
}
@@ -756,7 +770,10 @@ impl<'a, T: Config> gas_metering::Rules for ScheduleRules<'a, T> {
I32Rotr | I64Rotr => w.i64rotr,
// Returning None makes the gas instrumentation fail which we intend for
// unsupported or unknown instructions.
// unsupported or unknown instructions. Offchain we might allow indeterminism and hence
// use the fallback weight for those instructions.
_ if matches!(self.determinism, Determinism::AllowIndeterminism) && w.fallback > 0 =>
w.fallback,
_ => return None,
};
Some(weight)
+489 -56
View File
@@ -24,7 +24,7 @@ use crate::{
exec::{FixSizedKey, Frame},
storage::Storage,
tests::test_utils::{get_contract, get_contract_checked},
wasm::{PrefabWasmModule, ReturnCode as RuntimeReturnCode},
wasm::{Determinism, PrefabWasmModule, ReturnCode as RuntimeReturnCode},
weights::WeightInfo,
BalanceOf, Code, CodeStorage, Config, ContractInfoOf, DefaultAddressGenerator, DeletionQueue,
Error, Pallet, Schedule,
@@ -340,6 +340,7 @@ parameter_types! {
// We want stack height to be always enabled for tests so that this
// instrumentation path is always tested implicitly.
schedule.limits.stack_height = Some(512);
schedule.instruction_weights.fallback = 1;
schedule
};
pub static DepositPerByte: BalanceOf<Test> = 1;
@@ -522,7 +523,12 @@ fn instantiate_and_call_and_deposit_event() {
// We determine the storage deposit limit after uploading because it depends on ALICEs free
// balance which is changed by uploading a module.
assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, None));
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
None,
Determinism::Deterministic
));
// Drop previous events
initialize_block(2);
@@ -690,7 +696,13 @@ fn instantiate_unique_trie_id() {
ExtBuilder::default().existential_deposit(500).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, None).unwrap();
Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
None,
Determinism::Deterministic,
)
.unwrap();
let addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
// Instantiate the contract and store its trie id for later comparison.
@@ -940,6 +952,7 @@ fn delegate_call() {
RuntimeOrigin::signed(ALICE),
callee_wasm,
Some(codec::Compact(100_000)),
Determinism::Deterministic,
));
assert_ok!(Contracts::call(
@@ -1376,10 +1389,18 @@ fn crypto_hashes() {
// We offset data in the contract tables by 1.
let mut params = vec![(n + 1) as u8];
params.extend_from_slice(input);
let result =
<Pallet<Test>>::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, params, false)
.result
.unwrap();
let result = <Pallet<Test>>::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
params,
false,
Determinism::Deterministic,
)
.result
.unwrap();
assert!(!result.did_revert());
let expected = hash_fn(input.as_ref());
assert_eq!(&result.data[..*expected_size], &*expected);
@@ -1407,9 +1428,18 @@ fn transfer_return_code() {
// Contract has only the minimal balance so any transfer will fail.
Balances::make_free_balance_be(&addr, min_balance);
let result = Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, vec![], false)
.result
.unwrap();
let result = Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
vec![],
false,
Determinism::Deterministic,
)
.result
.unwrap();
assert_return_code!(result, RuntimeReturnCode::TransferFailed);
// Contract has enough total balance in order to not go below the min balance
@@ -1417,9 +1447,18 @@ fn transfer_return_code() {
// the transfer still fails.
Balances::make_free_balance_be(&addr, min_balance + 100);
Balances::reserve(&addr, min_balance + 100).unwrap();
let result = Contracts::bare_call(ALICE, addr, 0, GAS_LIMIT, None, vec![], false)
.result
.unwrap();
let result = Contracts::bare_call(
ALICE,
addr,
0,
GAS_LIMIT,
None,
vec![],
false,
Determinism::Deterministic,
)
.result
.unwrap();
assert_return_code!(result, RuntimeReturnCode::TransferFailed);
});
}
@@ -1454,6 +1493,7 @@ fn call_return_code() {
None,
AsRef::<[u8]>::as_ref(&DJANGO).to_vec(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1484,6 +1524,7 @@ fn call_return_code() {
.cloned()
.collect(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1506,6 +1547,7 @@ fn call_return_code() {
.cloned()
.collect(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1525,6 +1567,7 @@ fn call_return_code() {
.cloned()
.collect(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1543,6 +1586,7 @@ fn call_return_code() {
.cloned()
.collect(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1591,6 +1635,7 @@ fn instantiate_return_code() {
None,
callee_hash.clone(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1609,6 +1654,7 @@ fn instantiate_return_code() {
None,
callee_hash.clone(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1616,10 +1662,18 @@ fn instantiate_return_code() {
// Contract has enough balance but the passed code hash is invalid
Balances::make_free_balance_be(&addr, min_balance + 10_000);
let result =
Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, vec![0; 33], false)
.result
.unwrap();
let result = Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
vec![0; 33],
false,
Determinism::Deterministic,
)
.result
.unwrap();
assert_return_code!(result, RuntimeReturnCode::CodeNotFound);
// Contract has enough balance but callee reverts because "1" is passed.
@@ -1631,6 +1685,7 @@ fn instantiate_return_code() {
None,
callee_hash.iter().chain(&1u32.to_le_bytes()).cloned().collect(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1645,6 +1700,7 @@ fn instantiate_return_code() {
None,
callee_hash.iter().chain(&2u32.to_le_bytes()).cloned().collect(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1717,8 +1773,16 @@ fn chain_extension_works() {
// 0 = read input buffer and pass it through as output
let input: Vec<u8> = ExtensionInput { extension_id: 0, func_id: 0, extra: &[99] }.into();
let result =
Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, input.clone(), false);
let result = Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
input.clone(),
false,
Determinism::Deterministic,
);
assert_eq!(TestExtension::last_seen_buffer(), input);
assert_eq!(result.result.unwrap().data, input);
@@ -1731,6 +1795,7 @@ fn chain_extension_works() {
None,
ExtensionInput { extension_id: 0, func_id: 1, extra: &[] }.into(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1746,6 +1811,7 @@ fn chain_extension_works() {
None,
ExtensionInput { extension_id: 0, func_id: 2, extra: &[0] }.into(),
false,
Determinism::Deterministic,
);
assert_ok!(result.result);
let gas_consumed = result.gas_consumed;
@@ -1757,6 +1823,7 @@ fn chain_extension_works() {
None,
ExtensionInput { extension_id: 0, func_id: 2, extra: &[42] }.into(),
false,
Determinism::Deterministic,
);
assert_ok!(result.result);
assert_eq!(result.gas_consumed.ref_time(), gas_consumed.ref_time() + 42);
@@ -1768,6 +1835,7 @@ fn chain_extension_works() {
None,
ExtensionInput { extension_id: 0, func_id: 2, extra: &[95] }.into(),
false,
Determinism::Deterministic,
);
assert_ok!(result.result);
assert_eq!(result.gas_consumed.ref_time(), gas_consumed.ref_time() + 95);
@@ -1781,6 +1849,7 @@ fn chain_extension_works() {
None,
ExtensionInput { extension_id: 0, func_id: 3, extra: &[] }.into(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1798,6 +1867,7 @@ fn chain_extension_works() {
None,
ExtensionInput { extension_id: 1, func_id: 0, extra: &[] }.into(),
false,
Determinism::Deterministic,
)
.result
.unwrap();
@@ -1847,8 +1917,17 @@ fn chain_extension_temp_storage_works() {
);
assert_ok!(
Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, input.clone(), false)
.result
Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
input.clone(),
false,
Determinism::Deterministic
)
.result
);
})
}
@@ -2387,12 +2466,28 @@ fn reinstrument_does_charge() {
// Call the contract two times without reinstrument
let result0 =
Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, zero.clone(), false);
let result0 = Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
zero.clone(),
false,
Determinism::Deterministic,
);
assert!(!result0.result.unwrap().did_revert());
let result1 =
Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, zero.clone(), false);
let result1 = Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
zero.clone(),
false,
Determinism::Deterministic,
);
assert!(!result1.result.unwrap().did_revert());
// They should match because both where called with the same schedule.
@@ -2405,8 +2500,16 @@ fn reinstrument_does_charge() {
});
// This call should trigger reinstrumentation
let result2 =
Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, zero.clone(), false);
let result2 = Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
zero.clone(),
false,
Determinism::Deterministic,
);
assert!(!result2.result.unwrap().did_revert());
assert!(result2.gas_consumed.ref_time() > result1.gas_consumed.ref_time());
assert_eq!(
@@ -2433,7 +2536,16 @@ fn debug_message_works() {
vec![],
),);
let addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
let result = Contracts::bare_call(ALICE, addr, 0, GAS_LIMIT, None, vec![], true);
let result = Contracts::bare_call(
ALICE,
addr,
0,
GAS_LIMIT,
None,
vec![],
true,
Determinism::Deterministic,
);
assert_matches!(result.result, Ok(_));
assert_eq!(std::str::from_utf8(&result.debug_message).unwrap(), "Hello World!");
@@ -2457,7 +2569,16 @@ fn debug_message_logging_disabled() {
),);
let addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
// disable logging by passing `false`
let result = Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, vec![], false);
let result = Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
vec![],
false,
Determinism::Deterministic,
);
assert_matches!(result.result, Ok(_));
// the dispatchables always run without debugging
assert_ok!(Contracts::call(RuntimeOrigin::signed(ALICE), addr, 0, GAS_LIMIT, None, vec![]));
@@ -2481,7 +2602,16 @@ fn debug_message_invalid_utf8() {
vec![],
),);
let addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
let result = Contracts::bare_call(ALICE, addr, 0, GAS_LIMIT, None, vec![], true);
let result = Contracts::bare_call(
ALICE,
addr,
0,
GAS_LIMIT,
None,
vec![],
true,
Determinism::Deterministic,
);
assert_err!(result.result, <Error<Test>>::DebugMessageInvalidUTF8);
});
}
@@ -2532,6 +2662,7 @@ fn gas_estimation_nested_call_fixed_limit() {
None,
input.clone(),
false,
Determinism::Deterministic,
);
assert_ok!(&result.result);
@@ -2548,6 +2679,7 @@ fn gas_estimation_nested_call_fixed_limit() {
Some(result.storage_deposit.charge_or_zero()),
input,
false,
Determinism::Deterministic,
)
.result
);
@@ -2604,6 +2736,7 @@ fn gas_estimation_call_runtime() {
None,
call.encode(),
false,
Determinism::Deterministic,
);
// contract encodes the result of the dispatch runtime
let outcome = u32::decode(&mut result.result.unwrap().data.as_ref()).unwrap();
@@ -2620,6 +2753,7 @@ fn gas_estimation_call_runtime() {
None,
call.encode(),
false,
Determinism::Deterministic,
)
.result
);
@@ -2668,10 +2802,18 @@ fn ecdsa_recover() {
params.extend_from_slice(&signature);
params.extend_from_slice(&message_hash);
assert!(params.len() == 65 + 32);
let result =
<Pallet<Test>>::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, params, false)
.result
.unwrap();
let result = <Pallet<Test>>::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
params,
false,
Determinism::Deterministic,
)
.result
.unwrap();
assert!(!result.did_revert());
assert_eq!(result.data, EXPECTED_COMPRESSED_PUBLIC_KEY);
})
@@ -2691,7 +2833,8 @@ fn upload_code_works() {
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
Some(codec::Compact(1_000))
Some(codec::Compact(1_000)),
Determinism::Deterministic,
));
assert!(<CodeStorage<Test>>::contains_key(code_hash));
@@ -2702,7 +2845,7 @@ fn upload_code_works() {
phase: Phase::Initialization,
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
who: ALICE,
amount: 240,
amount: 241,
}),
topics: vec![],
},
@@ -2727,7 +2870,12 @@ fn upload_code_limit_too_low() {
initialize_block(2);
assert_noop!(
Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, Some(codec::Compact(100))),
Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
Some(codec::Compact(100)),
Determinism::Deterministic
),
<Error<Test>>::StorageDepositLimitExhausted,
);
@@ -2746,7 +2894,12 @@ fn upload_code_not_enough_balance() {
initialize_block(2);
assert_noop!(
Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm, Some(codec::Compact(1_000))),
Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
Some(codec::Compact(1_000)),
Determinism::Deterministic
),
<Error<Test>>::StorageDepositNotEnoughFunds,
);
@@ -2767,7 +2920,8 @@ fn remove_code_works() {
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
Some(codec::Compact(1_000))
Some(codec::Compact(1_000)),
Determinism::Deterministic,
));
assert!(<CodeStorage<Test>>::contains_key(code_hash));
@@ -2781,7 +2935,7 @@ fn remove_code_works() {
phase: Phase::Initialization,
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
who: ALICE,
amount: 240,
amount: 241,
}),
topics: vec![],
},
@@ -2794,7 +2948,7 @@ fn remove_code_works() {
phase: Phase::Initialization,
event: RuntimeEvent::Balances(pallet_balances::Event::Unreserved {
who: ALICE,
amount: 240,
amount: 241,
}),
topics: vec![],
},
@@ -2821,7 +2975,8 @@ fn remove_code_wrong_origin() {
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
Some(codec::Compact(1_000))
Some(codec::Compact(1_000)),
Determinism::Deterministic,
));
assert_noop!(
@@ -2836,7 +2991,7 @@ fn remove_code_wrong_origin() {
phase: Phase::Initialization,
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
who: ALICE,
amount: 240,
amount: 241,
}),
topics: vec![],
},
@@ -2969,7 +3124,7 @@ fn instantiate_with_zero_balance_works() {
phase: Phase::Initialization,
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
who: ALICE,
amount: 240,
amount: 241,
}),
topics: vec![],
},
@@ -3071,7 +3226,7 @@ fn instantiate_with_below_existential_deposit_works() {
phase: Phase::Initialization,
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
who: ALICE,
amount: 240,
amount: 241,
}),
topics: vec![],
},
@@ -3261,7 +3416,12 @@ fn set_code_extrinsic() {
));
let addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), new_wasm, None,));
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
new_wasm,
None,
Determinism::Deterministic
));
// Drop previous events
initialize_block(2);
@@ -3457,7 +3617,12 @@ fn contract_reverted() {
let input = (flags.bits(), buffer).encode();
// We just upload the code for later use
assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm.clone(), None));
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm.clone(),
None,
Determinism::Deterministic
));
// Calling extrinsic: revert leads to an error
assert_err_ignore_postinfo!(
@@ -3536,9 +3701,18 @@ fn contract_reverted() {
);
// Calling directly: revert leads to success but the flags indicate the error
let result = Contracts::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, None, input, false)
.result
.unwrap();
let result = Contracts::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
input,
false,
Determinism::Deterministic,
)
.result
.unwrap();
assert_eq!(result.flags, flags);
assert_eq!(result.data, buffer);
});
@@ -3551,7 +3725,12 @@ fn code_rejected_error_works() {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
assert_noop!(
Contracts::upload_code(RuntimeOrigin::signed(ALICE), wasm.clone(), None),
Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm.clone(),
None,
Determinism::Deterministic
),
<Error<Test>>::CodeRejected,
);
@@ -3594,7 +3773,12 @@ fn set_code_hash() {
vec![],
));
// upload new code
assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), new_wasm.clone(), None));
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
new_wasm.clone(),
None,
Determinism::Deterministic
));
System::reset_events();
@@ -3607,16 +3791,25 @@ fn set_code_hash() {
None,
new_code_hash.as_ref().to_vec(),
true,
Determinism::Deterministic,
)
.result
.unwrap();
assert_return_code!(result, 1);
// Second calls new contract code that returns 2
let result =
Contracts::bare_call(ALICE, contract_addr.clone(), 0, GAS_LIMIT, None, vec![], true)
.result
.unwrap();
let result = Contracts::bare_call(
ALICE,
contract_addr.clone(),
0,
GAS_LIMIT,
None,
vec![],
true,
Determinism::Deterministic,
)
.result
.unwrap();
assert_return_code!(result, 2);
// Checking for the last event only
@@ -3950,3 +4143,243 @@ fn deposit_limit_honors_min_leftover() {
assert_eq!(Balances::free_balance(&BOB), 1_000);
});
}
#[test]
fn cannot_instantiate_indeterministic_code() {
let (wasm, code_hash) = compile_module::<Test>("float_instruction").unwrap();
let (caller_wasm, _) = compile_module::<Test>("instantiate_return_code").unwrap();
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
// Try to instantiate directly from code
assert_err_ignore_postinfo!(
Contracts::instantiate_with_code(
RuntimeOrigin::signed(ALICE),
0,
GAS_LIMIT,
None,
wasm.clone(),
vec![],
vec![],
),
<Error<Test>>::CodeRejected,
);
assert_err!(
Contracts::bare_instantiate(
ALICE,
0,
GAS_LIMIT,
None,
Code::Upload(wasm.clone()),
vec![],
vec![],
false,
)
.result,
<Error<Test>>::CodeRejected,
);
// Try to upload a non deterministic code as deterministic
assert_err!(
Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm.clone(),
None,
Determinism::Deterministic
),
<Error<Test>>::CodeRejected,
);
// Try to instantiate from already stored indeterministic code hash
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
None,
Determinism::AllowIndeterminism,
));
assert_err_ignore_postinfo!(
Contracts::instantiate(
RuntimeOrigin::signed(ALICE),
0,
GAS_LIMIT,
None,
code_hash,
vec![],
vec![],
),
<Error<Test>>::Indeterministic,
);
assert_err!(
Contracts::bare_instantiate(
ALICE,
0,
GAS_LIMIT,
None,
Code::Existing(code_hash),
vec![],
vec![],
false,
)
.result,
<Error<Test>>::Indeterministic,
);
// Deploy contract which instantiates another contract
let addr = Contracts::bare_instantiate(
ALICE,
0,
GAS_LIMIT,
None,
Code::Upload(caller_wasm),
vec![],
vec![],
false,
)
.result
.unwrap()
.account_id;
// Try to instantiate `code_hash` from another contract in deterministic mode
assert_err!(
<Pallet<Test>>::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
code_hash.encode(),
false,
Determinism::Deterministic,
)
.result,
<Error<Test>>::Indeterministic,
);
// Instantiations are not allowed even in non determinism mode
assert_err!(
<Pallet<Test>>::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
code_hash.encode(),
false,
Determinism::AllowIndeterminism,
)
.result,
<Error<Test>>::Indeterministic,
);
});
}
#[test]
fn cannot_set_code_indeterministic_code() {
let (wasm, code_hash) = compile_module::<Test>("float_instruction").unwrap();
let (caller_wasm, _) = compile_module::<Test>("set_code_hash").unwrap();
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
// Put the non deterministic contract on-chain
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
None,
Determinism::AllowIndeterminism,
));
// Create the contract that will call `seal_set_code_hash`
let caller_addr = Contracts::bare_instantiate(
ALICE,
0,
GAS_LIMIT,
None,
Code::Upload(caller_wasm),
vec![],
vec![],
false,
)
.result
.unwrap()
.account_id;
// We do not allow to set the code hash to a non determinstic wasm
assert_err!(
<Pallet<Test>>::bare_call(
ALICE,
caller_addr.clone(),
0,
GAS_LIMIT,
None,
code_hash.encode(),
false,
Determinism::AllowIndeterminism,
)
.result,
<Error<Test>>::Indeterministic,
);
});
}
#[test]
fn delegate_call_indeterministic_code() {
let (wasm, code_hash) = compile_module::<Test>("float_instruction").unwrap();
let (caller_wasm, _) = compile_module::<Test>("delegate_call_simple").unwrap();
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
// Put the non deterministic contract on-chain
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
wasm,
None,
Determinism::AllowIndeterminism,
));
// Create the contract that will call `seal_delegate_call`
let caller_addr = Contracts::bare_instantiate(
ALICE,
0,
GAS_LIMIT,
None,
Code::Upload(caller_wasm),
vec![],
vec![],
false,
)
.result
.unwrap()
.account_id;
// The delegate call will fail in deterministic mode
assert_err!(
<Pallet<Test>>::bare_call(
ALICE,
caller_addr.clone(),
0,
GAS_LIMIT,
None,
code_hash.encode(),
false,
Determinism::Deterministic,
)
.result,
<Error<Test>>::Indeterministic,
);
// The delegate call will work on non deterministic mode
assert_ok!(
<Pallet<Test>>::bare_call(
ALICE,
caller_addr.clone(),
0,
GAS_LIMIT,
None,
code_hash.encode(),
false,
Determinism::AllowIndeterminism,
)
.result
);
});
}
@@ -201,7 +201,7 @@ pub fn reinstrument<T: Config>(
// as the contract is already deployed and every change in size would be the result
// of changes in the instrumentation algorithm controlled by the chain authors.
prefab_module.code = WeakBoundedVec::force_from(
prepare::reinstrument_contract::<T>(&original_code, schedule)
prepare::reinstrument_contract::<T>(&original_code, schedule, prefab_module.determinism)
.map_err(|_| <Error<T>>::CodeRejected)?,
Some("Contract exceeds limit after re-instrumentation."),
);
+39 -2
View File
@@ -37,6 +37,7 @@ use crate::{
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::dispatch::{DispatchError, DispatchResult};
use sp_core::crypto::UncheckedFrom;
use sp_runtime::RuntimeDebug;
use sp_sandbox::{SandboxEnvironmentBuilder, SandboxInstance, SandboxMemory};
use sp_std::prelude::*;
#[cfg(test)]
@@ -66,6 +67,10 @@ pub struct PrefabWasmModule<T: Config> {
maximum: u32,
/// Code instrumented with the latest schedule.
code: RelaxedCodeVec<T>,
/// A code that might contain non deterministic features and is therefore never allowed
/// to be run on chain. Specifically this code can never be instantiated into a contract
/// and can just be used through a delegate call.
determinism: Determinism,
/// The uninstrumented, pristine version of the code.
///
/// It is not stored because the pristine code has its own storage item. The value
@@ -102,6 +107,27 @@ pub struct OwnerInfo<T: Config> {
refcount: u64,
}
/// Defines the required determinism level of a wasm blob when either running or uploading code.
#[derive(
Clone, Copy, Encode, Decode, scale_info::TypeInfo, MaxEncodedLen, RuntimeDebug, PartialEq, Eq,
)]
pub enum Determinism {
/// The execution should be deterministic and hence no indeterministic instructions are
/// allowed.
///
/// Dispatchables always use this mode in order to make on-chain execution deterministic.
Deterministic,
/// Allow calling or uploading an indeterministic code.
///
/// This is only possible when calling into `pallet-contracts` directly via
/// [`crate::Pallet::bare_call`].
///
/// # Note
///
/// **Never** use this mode for on-chain execution.
AllowIndeterminism,
}
impl ExportedFunction {
/// The wasm export name for the function.
fn identifier(&self) -> &str {
@@ -124,11 +150,13 @@ where
original_code: Vec<u8>,
schedule: &Schedule<T>,
owner: AccountIdOf<T>,
determinism: Determinism,
) -> Result<Self, (DispatchError, &'static str)> {
let module = prepare::prepare_contract(
original_code.try_into().map_err(|_| (<Error<T>>::CodeTooLarge.into(), ""))?,
schedule,
owner,
determinism,
)?;
Ok(module)
}
@@ -258,6 +286,10 @@ where
fn code_len(&self) -> u32 {
self.code.len() as u32
}
fn is_deterministic(&self) -> bool {
matches!(self.determinism, Determinism::Deterministic)
}
}
#[cfg(test)]
@@ -551,8 +583,13 @@ mod tests {
fn execute<E: BorrowMut<MockExt>>(wat: &str, input_data: Vec<u8>, mut ext: E) -> ExecResult {
let wasm = wat::parse_str(wat).unwrap();
let schedule = crate::Schedule::default();
let executable =
PrefabWasmModule::<<MockExt as Ext>::T>::from_code(wasm, &schedule, ALICE).unwrap();
let executable = PrefabWasmModule::<<MockExt as Ext>::T>::from_code(
wasm,
&schedule,
ALICE,
Determinism::Deterministic,
)
.unwrap();
executable.execute(ext.borrow_mut(), &ExportedFunction::Call, input_data)
}
+20 -10
View File
@@ -22,7 +22,7 @@
use crate::{
chain_extension::ChainExtension,
storage::meter::Diff,
wasm::{env_def::ImportSatisfyCheck, OwnerInfo, PrefabWasmModule},
wasm::{env_def::ImportSatisfyCheck, Determinism, OwnerInfo, PrefabWasmModule},
AccountIdOf, CodeVec, Config, Error, Schedule,
};
use codec::{Encode, MaxEncodedLen};
@@ -182,8 +182,8 @@ impl<'a, T: Config> ContractModule<'a, T> {
Ok(())
}
fn inject_gas_metering(self) -> Result<Self, &'static str> {
let gas_rules = self.schedule.rules(&self.module);
fn inject_gas_metering(self, determinism: Determinism) -> Result<Self, &'static str> {
let gas_rules = self.schedule.rules(&self.module, determinism);
let contract_module =
wasm_instrument::gas_metering::inject(self.module, &gas_rules, "seal0")
.map_err(|_| "gas instrumentation failed")?;
@@ -369,6 +369,7 @@ fn get_memory_limits<T: Config>(
fn check_and_instrument<C: ImportSatisfyCheck, T: Config>(
original_code: &[u8],
schedule: &Schedule<T>,
determinism: Determinism,
) -> Result<(Vec<u8>, (u32, u32)), &'static str> {
let result = (|| {
let contract_module = ContractModule::new(original_code, schedule)?;
@@ -376,17 +377,20 @@ fn check_and_instrument<C: ImportSatisfyCheck, T: Config>(
contract_module.ensure_no_internal_memory()?;
contract_module.ensure_table_size_limit(schedule.limits.table_size)?;
contract_module.ensure_global_variable_limit(schedule.limits.globals)?;
contract_module.ensure_no_floating_types()?;
contract_module.ensure_parameter_limit(schedule.limits.parameters)?;
contract_module.ensure_br_table_size_limit(schedule.limits.br_table_size)?;
if matches!(determinism, Determinism::Deterministic) {
contract_module.ensure_no_floating_types()?;
}
// We disallow importing `gas` function here since it is treated as implementation detail.
let disallowed_imports = [b"gas".as_ref()];
let memory_limits =
get_memory_limits(contract_module.scan_imports::<C>(&disallowed_imports)?, schedule)?;
let code = contract_module
.inject_gas_metering()?
.inject_gas_metering(determinism)?
.inject_stack_height_metering()?
.into_wasm_code()?;
@@ -404,9 +408,11 @@ fn do_preparation<C: ImportSatisfyCheck, T: Config>(
original_code: CodeVec<T>,
schedule: &Schedule<T>,
owner: AccountIdOf<T>,
determinism: Determinism,
) -> Result<PrefabWasmModule<T>, (DispatchError, &'static str)> {
let (code, (initial, maximum)) = check_and_instrument::<C, T>(original_code.as_ref(), schedule)
.map_err(|msg| (<Error<T>>::CodeRejected.into(), msg))?;
let (code, (initial, maximum)) =
check_and_instrument::<C, T>(original_code.as_ref(), schedule, determinism)
.map_err(|msg| (<Error<T>>::CodeRejected.into(), msg))?;
let original_code_len = original_code.len();
let mut module = PrefabWasmModule {
@@ -414,6 +420,7 @@ fn do_preparation<C: ImportSatisfyCheck, T: Config>(
initial,
maximum,
code: code.try_into().map_err(|_| (<Error<T>>::CodeTooLarge.into(), ""))?,
determinism,
code_hash: T::Hashing::hash(&original_code),
original_code: Some(original_code),
owner_info: None,
@@ -449,8 +456,9 @@ pub fn prepare_contract<T: Config>(
original_code: CodeVec<T>,
schedule: &Schedule<T>,
owner: AccountIdOf<T>,
determinism: Determinism,
) -> Result<PrefabWasmModule<T>, (DispatchError, &'static str)> {
do_preparation::<super::runtime::Env, T>(original_code, schedule, owner)
do_preparation::<super::runtime::Env, T>(original_code, schedule, owner, determinism)
}
/// The same as [`prepare_contract`] but without constructing a new [`PrefabWasmModule`]
@@ -461,8 +469,9 @@ pub fn prepare_contract<T: Config>(
pub fn reinstrument_contract<T: Config>(
original_code: &[u8],
schedule: &Schedule<T>,
determinism: Determinism,
) -> Result<Vec<u8>, &'static str> {
Ok(check_and_instrument::<super::runtime::Env, T>(original_code, schedule)?.0)
Ok(check_and_instrument::<super::runtime::Env, T>(original_code, schedule, determinism)?.0)
}
/// Alternate (possibly unsafe) preparation functions used only for benchmarking.
@@ -495,6 +504,7 @@ pub mod benchmarking {
maximum: memory_limits.1,
code_hash: T::Hashing::hash(&original_code),
original_code: Some(original_code.try_into().map_err(|_| "Original code too large")?),
determinism: Determinism::Deterministic,
code: contract_module
.into_wasm_code()?
.try_into()
@@ -572,7 +582,7 @@ mod tests {
},
.. Default::default()
};
let r = do_preparation::<env::Env, Test>(wasm, &schedule, ALICE);
let r = do_preparation::<env::Env, Test>(wasm, &schedule, ALICE, Determinism::Deterministic);
assert_matches::assert_matches!(r.map_err(|(_, msg)| msg), $($expected)*);
}
};
@@ -2384,6 +2384,7 @@ pub mod env {
/// 2. Contracts using this API can't be assumed as having deterministic addresses. Said another
/// way, when using this API you lose the guarantee that an address always identifies a specific
/// code hash.
///
/// 3. If a contract calls into itself after changing its code the new call would use
/// the new code. However, if the original caller panics after returning from the sub call it
/// would revert the changes made by `seal_set_code_hash` and the next caller would use