contracts: Allow contracts to dispatch calls into the runtime (#9276)

* contracts: Allow contracts to dispatch calls into the runtime

* Fix RPC tests

* Fix typo

* Replace () by AllowAllFilter and DenyAllFilter

* Add rust doc

* Fixup for `()` removal

* Fix lowest gas calculation

* Rename AllowAllFilter and DenyAllFilter

* Updated changelog
This commit is contained in:
Alexander Theißen
2021-07-12 22:40:27 +02:00
committed by GitHub
parent 2f31602896
commit e01ac8cea0
80 changed files with 674 additions and 107 deletions
@@ -57,6 +57,7 @@
use crate::{
Error,
wasm::{Runtime, RuntimeCosts},
gas::ChargedAmount,
};
use codec::{Decode, MaxEncodedLen};
use frame_support::weights::Weight;
@@ -167,11 +168,22 @@ where
/// `weight`. It returns `Err` otherwise. In this case the chain extension should
/// abort the execution and pass through the error.
///
/// The returned value can be used to with [`Self::adjust_weight`]. Other than that
/// it has no purpose.
///
/// # Note
///
/// Weight is synonymous with gas in substrate.
pub fn charge_weight(&mut self, amount: Weight) -> Result<()> {
self.inner.runtime.charge_gas(RuntimeCosts::ChainExtension(amount)).map(|_| ())
pub fn charge_weight(&mut self, amount: Weight) -> Result<ChargedAmount> {
self.inner.runtime.charge_gas(RuntimeCosts::ChainExtension(amount))
}
/// Adjust a previously charged amount down to its actual amount.
///
/// This is when a maximum a priori amount was charged and then should be partially
/// refunded to match the actual amount.
pub fn adjust_weight(&mut self, charged: ChargedAmount, actual_weight: Weight) {
self.inner.runtime.adjust_gas(charged, RuntimeCosts::ChainExtension(actual_weight))
}
/// Grants access to the execution environment of the current contract call.
+129 -6
View File
@@ -28,12 +28,13 @@ use sp_std::{
};
use sp_runtime::{Perbill, traits::{Convert, Saturating}};
use frame_support::{
dispatch::{DispatchResult, DispatchError},
dispatch::{DispatchResult, DispatchError, DispatchResultWithPostInfo, Dispatchable},
storage::{with_transaction, TransactionOutcome},
traits::{ExistenceRequirement, Currency, Time, Randomness, Get},
traits::{ExistenceRequirement, Currency, Time, Randomness, Get, OriginTrait, Filter},
weights::Weight,
ensure, DefaultNoBound,
};
use frame_system::RawOrigin;
use pallet_contracts_primitives::{ExecReturnValue};
use smallvec::{SmallVec, Array};
@@ -300,6 +301,9 @@ pub trait Ext: sealing::Sealed {
///
/// Returns `true` if debug message recording is enabled. Otherwise `false` is returned.
fn append_debug_buffer(&mut self, msg: &str) -> bool;
/// Call some dispatchable and return the result.
fn call_runtime(&self, call: <Self::T as Config>::Call) -> DispatchResultWithPostInfo;
}
/// Describes the different functions that can be exported by an [`Executable`].
@@ -1291,6 +1295,12 @@ where
false
}
}
fn call_runtime(&self, call: <Self::T as Config>::Call) -> DispatchResultWithPostInfo {
let mut origin: T::Origin = RawOrigin::Signed(self.address().clone()).into();
origin.add_filter(T::CallFilter::filter);
call.dispatch(origin)
}
}
fn deposit_event<T: Config>(
@@ -1326,10 +1336,10 @@ mod sealing {
mod tests {
use super::*;
use crate::{
gas::GasMeter, tests::{ExtBuilder, Test, Event as MetaEvent},
gas::GasMeter,
storage::Storage,
tests::{
ALICE, BOB, CHARLIE,
ALICE, BOB, CHARLIE, Call, TestFilter, ExtBuilder, Test, Event as MetaEvent,
test_utils::{place_contract, set_balance, get_balance},
},
exec::ExportedFunction::*,
@@ -1337,12 +1347,15 @@ mod tests {
};
use codec::{Encode, Decode};
use sp_core::Bytes;
use sp_runtime::DispatchError;
use sp_runtime::{DispatchError, traits::{BadOrigin, Hash}};
use assert_matches::assert_matches;
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use pretty_assertions::{assert_eq, assert_ne};
use pallet_contracts_primitives::ReturnFlags;
use frame_support::{assert_ok, assert_err};
use frame_system::{EventRecord, Phase};
type System = frame_system::Pallet<Test>;
type MockStack<'a> = Stack<'a, Test, MockExecutable>;
@@ -1353,7 +1366,7 @@ mod tests {
}
fn events() -> Vec<Event<Test>> {
<frame_system::Pallet<Test>>::events()
System::events()
.into_iter()
.filter_map(|meta| match meta.event {
MetaEvent::Contracts(contract_event) => Some(contract_event),
@@ -2503,4 +2516,114 @@ mod tests {
);
});
}
#[test]
fn call_runtime_works() {
let code_hash = MockLoader::insert(Call, |ctx, _| {
let call = Call::System(frame_system::Call::remark_with_event(b"Hello World".to_vec()));
ctx.ext.call_runtime(call).unwrap();
exec_success()
});
ExtBuilder::default().build().execute_with(|| {
let subsistence = Contracts::<Test>::subsistence_threshold();
let schedule = <Test as Config>::Schedule::get();
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
set_balance(&ALICE, subsistence * 10);
place_contract(&BOB, code_hash);
System::reset_events();
MockStack::run_call(
ALICE,
BOB,
&mut gas_meter,
&schedule,
0,
vec![],
None,
).unwrap();
let remark_hash = <Test as frame_system::Config>::Hashing::hash(b"Hello World");
assert_eq!(System::events(), vec![
EventRecord {
phase: Phase::Initialization,
event: MetaEvent::System(frame_system::Event::Remarked(BOB, remark_hash)),
topics: vec![],
},
]);
});
}
#[test]
fn call_runtime_filter() {
let code_hash = MockLoader::insert(Call, |ctx, _| {
use frame_system::Call as SysCall;
use pallet_balances::Call as BalanceCall;
use pallet_utility::Call as UtilCall;
// remark should still be allowed
let allowed_call = Call::System(SysCall::remark_with_event(b"Hello".to_vec()));
// transfers are disallowed by the `TestFiler` (see below)
let forbidden_call = Call::Balances(BalanceCall::transfer(CHARLIE, 22));
// simple cases: direct call
assert_err!(
ctx.ext.call_runtime(forbidden_call.clone()),
BadOrigin,
);
// as part of a patch: return is OK (but it interrupted the batch)
assert_ok!(
ctx.ext.call_runtime(Call::Utility(UtilCall::batch(vec![
allowed_call.clone(), forbidden_call, allowed_call
]))),
);
// the transfer wasn't performed
assert_eq!(get_balance(&CHARLIE), 0);
exec_success()
});
TestFilter::set_filter(|call| {
match call {
Call::Balances(pallet_balances::Call::transfer(_, _)) => false,
_ => true,
}
});
ExtBuilder::default().build().execute_with(|| {
let subsistence = Contracts::<Test>::subsistence_threshold();
let schedule = <Test as Config>::Schedule::get();
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
set_balance(&ALICE, subsistence * 10);
place_contract(&BOB, code_hash);
System::reset_events();
MockStack::run_call(
ALICE,
BOB,
&mut gas_meter,
&schedule,
0,
vec![],
None,
).unwrap();
let remark_hash = <Test as frame_system::Config>::Hashing::hash(b"Hello");
assert_eq!(System::events(), vec![
EventRecord {
phase: Phase::Initialization,
event: MetaEvent::System(frame_system::Event::Remarked(BOB, remark_hash)),
topics: vec![],
},
EventRecord {
phase: Phase::Initialization,
event: MetaEvent::Utility(
pallet_utility::Event::BatchInterrupted(1, BadOrigin.into()),
),
topics: vec![],
},
]);
});
}
}
+34 -4
View File
@@ -79,6 +79,8 @@ pub struct GasMeter<T: Config> {
gas_limit: Weight,
/// Amount of gas left from initial gas limit. Can reach zero.
gas_left: Weight,
/// Due to `adjust_gas` and `nested` the `gas_left` can temporarily dip below its final value.
gas_left_lowest: Weight,
_phantom: PhantomData<T>,
#[cfg(test)]
tokens: Vec<ErasedToken>,
@@ -92,6 +94,7 @@ where
GasMeter {
gas_limit,
gas_left: gas_limit,
gas_left_lowest: gas_limit,
_phantom: PhantomData,
#[cfg(test)]
tokens: Vec::new(),
@@ -122,6 +125,19 @@ where
/// Absorb the remaining gas of a nested meter after we are done using it.
pub fn absorb_nested(&mut self, nested: Self) {
if self.gas_left == 0 {
// All of the remaining gas was inherited by the nested gas meter. When absorbing
// we can therefore safely inherit the lowest gas that the nested gas meter experienced
// as long as it is lower than the lowest gas that was experienced by the parent.
// We cannot call `self.gas_left_lowest()` here because in the state that this
// code is run the parent gas meter has `0` gas left.
self.gas_left_lowest = nested.gas_left_lowest().min(self.gas_left_lowest);
} else {
// The nested gas meter was created with a fixed amount that did not consume all of the
// parents (self) gas. The lowest gas that self will experience is when the nested
// gas was pre charged with the fixed amount.
self.gas_left_lowest = self.gas_left_lowest();
}
self.gas_left += nested.gas_left;
}
@@ -163,12 +179,21 @@ where
/// This is when a maximum a priori amount was charged and then should be partially
/// refunded to match the actual amount.
pub fn adjust_gas<Tok: Token<T>>(&mut self, charged_amount: ChargedAmount, token: Tok) {
self.gas_left_lowest = self.gas_left_lowest();
let adjustment = charged_amount.0.saturating_sub(token.weight());
self.gas_left = self.gas_left.saturating_add(adjustment).min(self.gas_limit);
}
/// Returns how much gas was used.
pub fn gas_spent(&self) -> Weight {
/// Returns the amount of gas that is required to run the same call.
///
/// This can be different from `gas_spent` because due to `adjust_gas` the amount of
/// spent gas can temporarily drop and be refunded later.
pub fn gas_required(&self) -> Weight {
self.gas_limit - self.gas_left_lowest()
}
/// Returns how much gas was spent
pub fn gas_consumed(&self) -> Weight {
self.gas_limit - self.gas_left
}
@@ -179,14 +204,15 @@ where
/// Turn this GasMeter into a DispatchResult that contains the actually used gas.
pub fn into_dispatch_result<R, E>(
self, result: Result<R, E>,
self,
result: Result<R, E>,
base_weight: Weight,
) -> DispatchResultWithPostInfo
where
E: Into<ExecError>,
{
let post_info = PostDispatchInfo {
actual_weight: Some(self.gas_spent().saturating_add(base_weight)),
actual_weight: Some(self.gas_consumed().saturating_add(base_weight)),
pays_fee: Default::default(),
};
@@ -195,6 +221,10 @@ where
.map_err(|e| DispatchErrorWithPostInfo { post_info, error: e.into().error })
}
fn gas_left_lowest(&self) -> Weight {
self.gas_left_lowest.min(self.gas_left)
}
#[cfg(test)]
pub fn tokens(&self) -> &[ErasedToken] {
&self.tokens
+44 -5
View File
@@ -118,8 +118,9 @@ use sp_runtime::{
Perbill,
};
use frame_support::{
traits::{OnUnbalanced, Currency, Get, Time, Randomness},
weights::{Weight, PostDispatchInfo, WithPostDispatchInfo},
traits::{OnUnbalanced, Currency, Get, Time, Randomness, Filter},
weights::{Weight, PostDispatchInfo, WithPostDispatchInfo, GetDispatchInfo},
dispatch::Dispatchable,
};
use frame_system::Pallet as System;
use pallet_contracts_primitives::{
@@ -154,6 +155,41 @@ pub mod pallet {
/// The overarching event type.
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
/// The overarching call type.
type Call:
Dispatchable<Origin=Self::Origin, PostInfo=PostDispatchInfo> +
GetDispatchInfo +
codec::Decode +
IsType<<Self as frame_system::Config>::Call>;
/// Filter that is applied to calls dispatched by contracts.
///
/// Use this filter to control which dispatchables are callable by contracts.
/// This is applied in **addition** to [`frame_system::Config::BaseCallFilter`].
/// It is recommended to treat this as a whitelist.
///
/// # Subsistence Threshold
///
/// The runtime **must** make sure that any allowed dispatchable makes sure that the
/// `total_balance` of the contract stays above [`Pallet::subsistence_threshold()`].
/// Otherwise contracts can clutter the storage with their tombstones without
/// deposting the correct amount of balance.
///
/// # Stability
///
/// The runtime **must** make sure that all dispatchables that are callable by
/// contracts remain stable. In addition [`Self::Call`] itself must remain stable.
/// This means that no existing variants are allowed to switch their positions.
///
/// # Note
///
/// Note that dispatchables that are called via contracts do not spawn their
/// own wasm instance for each call (as opposed to when called via a transaction).
/// Therefore please make sure to be restrictive about which dispatchables are allowed
/// in order to not introduce a new DoS vector like memory allocation patterns that can
/// be exploited to drive the runtime into a panic.
type CallFilter: Filter<<Self as frame_system::Config>::Call>;
/// Handler for rent payments.
type RentPayment: OnUnbalanced<NegativeImbalanceOf<Self>>;
@@ -658,7 +694,8 @@ where
);
ContractExecResult {
result: result.map_err(|r| r.error),
gas_consumed: gas_meter.gas_spent(),
gas_consumed: gas_meter.gas_consumed(),
gas_required: gas_meter.gas_required(),
debug_message: debug_message.unwrap_or_default(),
}
}
@@ -699,7 +736,8 @@ where
Ok(executable) => executable,
Err(error) => return ContractInstantiateResult {
result: Err(error.into()),
gas_consumed: gas_meter.gas_spent(),
gas_consumed: gas_meter.gas_consumed(),
gas_required: gas_meter.gas_required(),
debug_message: Vec::new(),
}
};
@@ -727,7 +765,8 @@ where
});
ContractInstantiateResult {
result: result.map_err(|e| e.error),
gas_consumed: gas_meter.gas_spent(),
gas_consumed: gas_meter.gas_consumed(),
gas_required: gas_meter.gas_required(),
debug_message: debug_message.unwrap_or_default(),
}
}
+106 -12
View File
@@ -40,13 +40,14 @@ use sp_io::hashing::blake2_256;
use frame_support::{
assert_ok, assert_err, assert_err_ignore_postinfo,
parameter_types, assert_storage_noop,
traits::{Currency, ReservableCurrency, OnInitialize},
traits::{Currency, ReservableCurrency, OnInitialize, Filter},
weights::{Weight, PostDispatchInfo, DispatchClass, constants::WEIGHT_PER_SECOND},
dispatch::DispatchErrorWithPostInfo,
storage::child,
};
use frame_system::{self as system, EventRecord, Phase};
use pretty_assertions::assert_eq;
use std::cell::RefCell;
use crate as pallet_contracts;
@@ -63,6 +64,7 @@ frame_support::construct_runtime!(
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent},
Randomness: pallet_randomness_collective_flip::{Pallet, Storage},
Utility: pallet_utility::{Pallet, Call, Storage, Event},
Contracts: pallet_contracts::{Pallet, Call, Storage, Event<T>},
}
);
@@ -125,7 +127,7 @@ pub mod test_utils {
}
thread_local! {
static TEST_EXTENSION: sp_std::cell::RefCell<TestExtension> = Default::default();
static TEST_EXTENSION: RefCell<TestExtension> = Default::default();
}
pub struct TestExtension {
@@ -211,7 +213,7 @@ parameter_types! {
pub static ExistentialDeposit: u64 = 0;
}
impl frame_system::Config for Test {
type BaseCallFilter = ();
type BaseCallFilter = frame_support::traits::AllowAll;
type BlockWeights = BlockWeights;
type BlockLength = ();
type DbWeight = ();
@@ -256,6 +258,11 @@ impl pallet_timestamp::Config for Test {
type MinimumPeriod = MinimumPeriod;
type WeightInfo = ();
}
impl pallet_utility::Config for Test {
type Event = Event;
type Call = Call;
type WeightInfo = ();
}
parameter_types! {
pub const SignedClaimHandicap: u64 = 2;
pub const TombstoneDeposit: u64 = 16;
@@ -269,9 +276,6 @@ parameter_types! {
pub const DeletionWeightLimit: Weight = 500_000_000_000;
pub const MaxCodeSize: u32 = 2 * 1024;
pub MySchedule: Schedule<Test> = <Schedule<Test>>::default();
}
parameter_types! {
pub const TransactionByteFee: u64 = 0;
}
@@ -281,11 +285,32 @@ impl Convert<Weight, BalanceOf<Self>> for Test {
}
}
/// A filter whose filter function can be swapped at runtime.
pub struct TestFilter;
thread_local! {
static CALL_FILTER: RefCell<fn(&Call) -> bool> = RefCell::new(|_| true);
}
impl TestFilter {
pub fn set_filter(filter: fn(&Call) -> bool) {
CALL_FILTER.with(|fltr| *fltr.borrow_mut() = filter);
}
}
impl Filter<Call> for TestFilter {
fn filter(call: &Call) -> bool {
CALL_FILTER.with(|fltr| fltr.borrow()(call))
}
}
impl Config for Test {
type Time = Timestamp;
type Randomness = Randomness;
type Currency = Balances;
type Event = Event;
type Call = Call;
type CallFilter = TestFilter;
type RentPayment = ();
type SignedClaimHandicap = SignedClaimHandicap;
type TombstoneDeposit = TombstoneDeposit;
@@ -2944,8 +2969,8 @@ fn debug_message_invalid_utf8() {
}
#[test]
fn gas_estimation_correct() {
let (caller_code, caller_hash) = compile_module::<Test>("call_return_code").unwrap();
fn gas_estimation_nested_call_fixed_limit() {
let (caller_code, caller_hash) = compile_module::<Test>("call_with_limit").unwrap();
let (callee_code, callee_hash) = compile_module::<Test>("dummy").unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
let subsistence = Pallet::<Test>::subsistence_threshold();
@@ -2976,24 +3001,93 @@ fn gas_estimation_correct() {
);
let addr_callee = Contracts::contract_address(&ALICE, &callee_hash, &[1]);
let input: Vec<u8> = AsRef::<[u8]>::as_ref(&addr_callee)
.iter()
.cloned()
.chain((GAS_LIMIT / 5).to_le_bytes())
.collect();
// Call in order to determine the gas that is required for this call
let result = Contracts::bare_call(
ALICE,
addr_caller.clone(),
0,
GAS_LIMIT,
AsRef::<[u8]>::as_ref(&addr_callee).to_vec(),
input.clone(),
false,
);
assert_ok!(result.result);
assert_ok!(&result.result);
assert!(result.gas_required > result.gas_consumed);
// Make the same call using the estimated gas. Should succeed.
assert_ok!(Contracts::bare_call(
ALICE,
addr_caller,
0,
result.gas_consumed,
AsRef::<[u8]>::as_ref(&addr_callee).to_vec(),
result.gas_required,
input,
false,
).result);
});
}
#[test]
#[cfg(feature = "unstable-interface")]
fn gas_estimation_call_runtime() {
let (caller_code, caller_hash) = compile_module::<Test>("call_runtime").unwrap();
let (callee_code, callee_hash) = compile_module::<Test>("dummy").unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
let subsistence = Pallet::<Test>::subsistence_threshold();
let _ = Balances::deposit_creating(&ALICE, 1000 * subsistence);
let _ = Balances::deposit_creating(&CHARLIE, 1000 * subsistence);
assert_ok!(
Contracts::instantiate_with_code(
Origin::signed(ALICE),
subsistence * 100,
GAS_LIMIT,
caller_code,
vec![],
vec![0],
),
);
let addr_caller = Contracts::contract_address(&ALICE, &caller_hash, &[0]);
assert_ok!(
Contracts::instantiate_with_code(
Origin::signed(ALICE),
subsistence * 100,
GAS_LIMIT,
callee_code,
vec![],
vec![1],
),
);
let addr_callee = Contracts::contract_address(&ALICE, &callee_hash, &[1]);
// Call something trivial with a huge gas limit so that we can observe the effects
// of pre-charging. This should create a difference between consumed and required.
let call = Call::Contracts(crate::Call::call(addr_callee, 0, GAS_LIMIT / 3, vec![]));
let result = Contracts::bare_call(
ALICE,
addr_caller.clone(),
0,
GAS_LIMIT,
call.encode(),
false,
);
assert_ok!(&result.result);
assert!(result.gas_required > result.gas_consumed);
// Make the same call using the required gas. Should succeed.
assert_ok!(Contracts::bare_call(
ALICE,
addr_caller,
0,
result.gas_required,
call.encode(),
false,
).result);
});
+93 -6
View File
@@ -254,18 +254,22 @@ mod tests {
rent::RentStatus,
tests::{Test, Call, ALICE, BOB},
};
use std::collections::HashMap;
use std::{
borrow::BorrowMut,
cell::RefCell,
collections::HashMap,
};
use sp_core::{Bytes, H256};
use hex_literal::hex;
use sp_runtime::DispatchError;
use frame_support::{assert_ok, dispatch::DispatchResult, weights::Weight};
use frame_support::{
assert_ok,
dispatch::{DispatchResult, DispatchResultWithPostInfo},
weights::Weight,
};
use assert_matches::assert_matches;
use pallet_contracts_primitives::{ExecReturnValue, ReturnFlags};
use pretty_assertions::assert_eq;
use sp_std::borrow::BorrowMut;
#[derive(Debug, PartialEq, Eq)]
struct DispatchEntry(Call);
#[derive(Debug, PartialEq, Eq)]
struct RestoreEntry {
@@ -313,6 +317,7 @@ mod tests {
restores: Vec<RestoreEntry>,
// (topics, data)
events: Vec<(Vec<H256>, Vec<u8>)>,
runtime_calls: RefCell<Vec<Call>>,
schedule: Schedule<Test>,
rent_params: RentParams<Test>,
gas_meter: GasMeter<Test>,
@@ -335,6 +340,7 @@ mod tests {
transfers: Default::default(),
restores: Default::default(),
events: Default::default(),
runtime_calls: Default::default(),
schedule: Default::default(),
rent_params: Default::default(),
gas_meter: GasMeter::new(10_000_000_000),
@@ -481,6 +487,10 @@ mod tests {
self.debug_buffer.extend(msg.as_bytes());
true
}
fn call_runtime(&self, call: <Self::T as Config>::Call) -> DispatchResultWithPostInfo {
self.runtime_calls.borrow_mut().push(call);
Ok(Default::default())
}
}
fn execute<E: BorrowMut<MockExt>>(
@@ -2160,4 +2170,81 @@ mod tests {
})
);
}
#[cfg(feature = "unstable-interface")]
const CODE_CALL_RUNTIME: &str = r#"
(module
(import "__unstable__" "seal_call_runtime" (func $seal_call_runtime (param i32 i32) (result i32)))
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "env" "memory" (memory 1 1))
;; 0x1000 = 4k in little endian
;; size of input buffer
(data (i32.const 0) "\00\10")
(func (export "call")
;; Receive the encoded call
(call $seal_input
(i32.const 4) ;; Pointer to the input buffer
(i32.const 0) ;; Size of the length buffer
)
;; Just use the call passed as input and store result to memory
(i32.store (i32.const 0)
(call $seal_call_runtime
(i32.const 4) ;; Pointer where the call is stored
(i32.load (i32.const 0)) ;; Size of the call
)
)
(call $seal_return
(i32.const 0) ;; flags
(i32.const 0) ;; returned value
(i32.const 4) ;; length of returned value
)
)
(func (export "deploy"))
)
"#;
#[test]
#[cfg(feature = "unstable-interface")]
fn call_runtime_works() {
use std::convert::TryInto;
let call = Call::System(frame_system::Call::remark(b"Hello World".to_vec()));
let mut ext = MockExt::default();
let result = execute(
CODE_CALL_RUNTIME,
call.encode(),
&mut ext,
).unwrap();
assert_eq!(
*ext.runtime_calls.borrow(),
vec![call],
);
// 0 = ReturnCode::Success
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 0);
}
#[test]
#[cfg(feature = "unstable-interface")]
fn call_runtime_panics_on_invalid_call() {
let mut ext = MockExt::default();
let result = execute(
CODE_CALL_RUNTIME,
vec![0x42],
&mut ext,
);
assert_eq!(
result,
Err(ExecError {
error: Error::<Test>::DecodingFailed.into(),
origin: ErrorOrigin::Caller,
})
);
assert_eq!(
*ext.runtime_calls.borrow(),
vec![],
);
}
}
+76 -1
View File
@@ -76,6 +76,9 @@ pub enum ReturnCode {
/// recording was disabled.
#[cfg(feature = "unstable-interface")]
LoggingDisabled = 9,
/// The call dispatched by `seal_call_runtime` was executed but returned an error.
#[cfg(feature = "unstable-interface")]
CallRuntimeReturnedError = 10,
}
impl ConvertibleToWasm for ReturnCode {
@@ -213,6 +216,12 @@ pub enum RuntimeCosts {
HashBlake128(u32),
/// Weight charged by a chain extension through `seal_call_chain_extension`.
ChainExtension(u64),
/// Weight charged for copying data from the sandbox.
#[cfg(feature = "unstable-interface")]
CopyIn(u32),
/// Weight charged for calling into the runtime.
#[cfg(feature = "unstable-interface")]
CallRuntime(Weight),
}
impl RuntimeCosts {
@@ -273,6 +282,10 @@ impl RuntimeCosts {
HashBlake128(len) => s.hash_blake2_128
.saturating_add(s.hash_blake2_128_per_byte.saturating_mul(len.into())),
ChainExtension(amount) => amount,
#[cfg(feature = "unstable-interface")]
CopyIn(len) => s.return_per_byte.saturating_mul(len.into()),
#[cfg(feature = "unstable-interface")]
CallRuntime(weight) => weight,
};
RuntimeToken {
#[cfg(test)]
@@ -457,6 +470,15 @@ where
self.ext.gas_meter().charge(token)
}
/// Adjust a previously charged amount down to its actual amount.
///
/// This is when a maximum a priori amount was charged and then should be partially
/// refunded to match the actual amount.
pub fn adjust_gas(&mut self, charged: ChargedAmount, actual_costs: RuntimeCosts) {
let token = actual_costs.token(&self.ext.schedule().host_fn_weights);
self.ext.gas_meter().adjust_gas(charged, token);
}
/// Read designated chunk from the sandbox memory.
///
/// Returns `Err` if one of the following conditions occurs:
@@ -797,7 +819,6 @@ where
// data passed to the supervisor will lead to a trap. This is not documented explicitly
// for every function.
define_env!(Env, <E: Ext>,
// Account for used gas. Traps if gas used is greater than gas limit.
//
// NOTE: This is a implementation defined call and is NOT a part of the public API.
@@ -1808,4 +1829,58 @@ define_env!(Env, <E: Ext>,
out_ptr, out_len_ptr, &rent_status, false, already_charged
)?)
},
// Call some dispatchable of the runtime.
//
// This function decodes the passed in data as the overarching `Call` type of the
// runtime and dispatches it. The weight as specified in the runtime is charged
// from the gas meter. Any weight refunds made by the dispatchable are considered.
//
// The filter specified by `Config::CallFilter` is attached to the origin of
// the dispatched call.
//
// # Parameters
//
// - `input_ptr`: the pointer into the linear memory where the input data is placed.
// - `input_len`: the length of the input data in bytes.
//
// # Return Value
//
// Returns `ReturnCode::Success` when the dispatchable was succesfully executed and
// returned `Ok`. When the dispatchable was exeuted but returned an error
// `ReturnCode::CallRuntimeReturnedError` is returned. The full error is not
// provided because it is not guaranteed to be stable.
//
// # Comparison with `ChainExtension`
//
// Just as a chain extension this API allows the runtime to extend the functionality
// of contracts. While making use of this function is generelly easier it cannot be
// used in call cases. Consider writing a chain extension if you need to do perform
// one of the following tasks:
//
// - Return data.
// - Provide functionality **exclusively** to contracts.
// - Provide custom weights.
// - Avoid the need to keep the `Call` data structure stable.
//
// # Unstable
//
// This function is unstable and subject to change (or removal) in the future. Do not
// deploy a contract using it to a production chain.
[__unstable__] seal_call_runtime(ctx, call_ptr: u32, call_len: u32) -> ReturnCode => {
use frame_support::{dispatch::GetDispatchInfo, weights::extract_actual_weight};
ctx.charge_gas(RuntimeCosts::CopyIn(call_len))?;
let call: <E::T as Config>::Call = ctx.read_sandbox_memory_as_unbounded(
call_ptr, call_len
)?;
let dispatch_info = call.get_dispatch_info();
let charged = ctx.charge_gas(RuntimeCosts::CallRuntime(dispatch_info.weight))?;
let result = ctx.ext.call_runtime(call);
let actual_weight = extract_actual_weight(&result, &dispatch_info);
ctx.adjust_gas(charged, RuntimeCosts::CallRuntime(actual_weight));
match result {
Ok(_) => Ok(ReturnCode::Success),
Err(_) => Ok(ReturnCode::CallRuntimeReturnedError),
}
},
);