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
+31 -4
View File
@@ -1959,7 +1959,16 @@ impl_runtime_apis! {
input_data: Vec<u8>,
) -> pallet_contracts_primitives::ContractExecResult<Balance> {
let gas_limit = gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block);
Contracts::bare_call(origin, dest, value, gas_limit, storage_deposit_limit, input_data, true)
Contracts::bare_call(
origin,
dest,
value,
gas_limit,
storage_deposit_limit,
input_data,
true,
pallet_contracts::Determinism::Deterministic,
)
}
fn instantiate(
@@ -1973,23 +1982,41 @@ impl_runtime_apis! {
) -> pallet_contracts_primitives::ContractInstantiateResult<AccountId, Balance>
{
let gas_limit = gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block);
Contracts::bare_instantiate(origin, value, gas_limit, storage_deposit_limit, code, data, salt, true)
Contracts::bare_instantiate(
origin,
value,
gas_limit,
storage_deposit_limit,
code,
data,
salt,
true
)
}
fn upload_code(
origin: AccountId,
code: Vec<u8>,
storage_deposit_limit: Option<Balance>,
determinism: pallet_contracts::Determinism,
) -> pallet_contracts_primitives::CodeUploadResult<Hash, Balance>
{
Contracts::bare_upload_code(origin, code, storage_deposit_limit)
Contracts::bare_upload_code(
origin,
code,
storage_deposit_limit,
determinism,
)
}
fn get_storage(
address: AccountId,
key: Vec<u8>,
) -> pallet_contracts_primitives::GetStorageResult {
Contracts::get_storage(address, key)
Contracts::get_storage(
address,
key
)
}
}
+27 -2
View File
@@ -37,12 +37,37 @@ changes still persist.
One gas is equivalent to one [weight](https://docs.substrate.io/v3/runtime/weights-and-fees)
which is defined as one picosecond of execution time on the runtime's reference machine.
### Notable Scenarios
### Revert Behaviour
Contract call failures are not always cascading. When failures occur in a sub-call, they do not "bubble up",
Contract call failures are not cascading. When failures occur in a sub-call, they do not "bubble up",
and the call will only revert at the specific contract level. For example, if contract A calls contract B, and B
fails, A can decide how to handle that failure, either proceeding or reverting A's changes.
### Offchain Execution
In general, a contract execution needs to be deterministic so that all nodes come to the same
conclusion when executing it. To that end we disallow any instructions that could cause
indeterminism. Most notable are any floating point arithmetic. That said, sometimes contracts
are executed off-chain and hence are not subject to consensus. If code is only executed by a
single node and implicitly trusted by other actors is such a case. Trusted execution environments
come to mind. To that end we allow the execution of indeterminstic code for offchain usages
with the following constraints:
1. No contract can ever be instantiated from an indeterministic code. The only way to execute
the code is to use a delegate call from a deterministic contract.
2. The code that wants to use this feature needs to depend on `pallet-contracts` and use `bare_call`
directly. This makes sure that by default `pallet-contracts` does not expose any indeterminism.
## How to use
When setting up the `Schedule` for your runtime make sure to set `InstructionWeights::fallback`
to a non zero value. The default is `0` and prevents the upload of any non deterministic code.
An indeterministic code can be deployed on-chain by passing `Determinism::AllowIndeterministic`
to `upload_code`. A determinstic contract can then delegate call into it if and only if it
is ran by using `bare_call` and passing `Determinism::AllowIndeterministic` to it. **Never use
this argument when the contract is called from an on-chain transaction.**
## Interface
### Dispatchable functions
@@ -0,0 +1,50 @@
;; Just delegate call into the passed code hash and assert success.
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_delegate_call" (func $seal_delegate_call (param i32 i32 i32 i32 i32 i32) (result i32)))
(import "env" "memory" (memory 3 3))
;; [0, 32) buffer where input is copied
;; [32, 36) size of the input buffer
(data (i32.const 32) "\20")
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "call")
;; Reading "callee" code_hash
(call $seal_input (i32.const 0) (i32.const 32))
;; assert input size == 32
(call $assert
(i32.eq
(i32.load (i32.const 32))
(i32.const 32)
)
)
;; Delegate call into passed code hash
(call $assert
(i32.eq
(call $seal_delegate_call
(i32.const 0) ;; Set no call flags
(i32.const 0) ;; Pointer to "callee" code_hash.
(i32.const 0) ;; Input is ignored
(i32.const 0) ;; Length of the input
(i32.const 4294967295) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Length is ignored in this case
)
(i32.const 0)
)
)
)
(func (export "deploy"))
)
@@ -0,0 +1,11 @@
;; Module that contains a float instruction which is illegal in deterministic mode
(module
(func (export "call")
f32.const 1
drop
)
(func (export "deploy")
f32.const 2
drop
)
)
@@ -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