Skip to content

Commit

Permalink
Contracts: xcm host fn fixes (paritytech#3086)
Browse files Browse the repository at this point in the history
## Xcm changes:
- Fix `pallet_xcm::execute`, move the logic into The `ExecuteController`
so it can be shared with anything that implement that trait.
- Make `ExecuteController::execute` retursn `DispatchErrorWithPostInfo`
instead of `DispatchError`, so that we don't charge the full
`max_weight` provided if the execution is incomplete (useful for
force_batch or contracts calls)
- Fix docstring for `pallet_xcm::execute`, to reflect the changes from
paritytech#2405
- Update the signature for `ExecuteController::execute`, we don't need
to return the `Outcome` anymore since we only care about
`Outcome::Complete`

## Contracts changes:

- Update host fn `xcm_exexute`, we don't need to write the `Outcome` to
the sandbox memory anymore. This was also not charged as well before so
it if fixes this too.
- One of the issue was that the dry_run of a contract that call
`xcm_execute` would exhaust the `gas_limit`.

This is because `XcmExecuteController::execute` takes a `max_weight`
argument, and since we don't want the user to specify it manually we
were passing everything left by pre-charghing
`ctx.ext.gas_meter().gas_left()`

- To fix it I added a `fn influence_lowest_limit` on the `Token` trait
and make it return false for `RuntimeCost::XcmExecute`.
- Got rid of the `RuntimeToken` indirection, we can just use
`RuntimeCost` directly.

---------

Co-authored-by: command-bot <>
  • Loading branch information
pgherveou authored Feb 19, 2024
1 parent 1400ad5 commit 5987222
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 85 deletions.
11 changes: 6 additions & 5 deletions substrate/frame/contracts/fixtures/contracts/xcm_execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ pub extern "C" fn deploy() {}
pub extern "C" fn call() {
input!(512, msg: [u8],);

let mut outcome = [0u8; 512];
let outcome = &mut &mut outcome[..];

#[allow(deprecated)]
api::xcm_execute(msg, outcome).unwrap();
api::return_value(uapi::ReturnFlags::empty(), outcome);
let err_code = match api::xcm_execute(msg) {
Ok(_) => 0u32,
Err(code) => code as u32,
};

api::return_value(uapi::ReturnFlags::empty(), &err_code.to_le_bytes());
}
3 changes: 2 additions & 1 deletion substrate/frame/contracts/mock-network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ use crate::primitives::{AccountId, UNITS};
use sp_runtime::BuildStorage;
use xcm::latest::prelude::*;
use xcm_executor::traits::ConvertLocation;
use xcm_simulator::{decl_test_network, decl_test_parachain, decl_test_relay_chain, TestExt};
pub use xcm_simulator::TestExt;
use xcm_simulator::{decl_test_network, decl_test_parachain, decl_test_relay_chain};

// Accounts
pub const ADMIN: sp_runtime::AccountId32 = sp_runtime::AccountId32::new([0u8; 32]);
Expand Down
71 changes: 54 additions & 17 deletions substrate/frame/contracts/mock-network/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use crate::{
primitives::{AccountId, CENTS},
relay_chain, MockNet, ParaA, ParachainBalances, Relay, ALICE, BOB, INITIAL_BALANCE,
};
use assert_matches::assert_matches;
use codec::{Decode, Encode};
use frame_support::{
assert_err,
Expand All @@ -31,11 +30,18 @@ use frame_support::{
use pallet_balances::{BalanceLock, Reasons};
use pallet_contracts::{Code, CollectEvents, DebugInfo, Determinism};
use pallet_contracts_fixtures::compile_module;
use pallet_contracts_uapi::ReturnErrorCode;
use xcm::{v4::prelude::*, VersionedLocation, VersionedXcm};
use xcm_simulator::TestExt;

type ParachainContracts = pallet_contracts::Pallet<parachain::Runtime>;

macro_rules! assert_return_code {
( $x:expr , $y:expr $(,)? ) => {{
assert_eq!(u32::from_le_bytes($x.data[..].try_into().unwrap()), $y as u32);
}};
}

/// Instantiate the tests contract, and fund it with some balance and assets.
fn instantiate_test_contract(name: &str) -> AccountId {
let (wasm, _) = compile_module::<Runtime>(name).unwrap();
Expand Down Expand Up @@ -100,19 +106,57 @@ fn test_xcm_execute() {
DebugInfo::UnsafeDebug,
CollectEvents::UnsafeCollect,
Determinism::Enforced,
)
.result
.unwrap();
);

let mut data = &result.data[..];
let outcome = Outcome::decode(&mut data).expect("Failed to decode xcm_execute Outcome");
assert_matches!(outcome, Outcome::Complete { .. });
assert_eq!(result.gas_consumed, result.gas_required);
assert_return_code!(&result.result.unwrap(), ReturnErrorCode::Success);

// Check if the funds are subtracted from the account of Alice and added to the account of
// Bob.
let initial = INITIAL_BALANCE;
assert_eq!(parachain::Assets::balance(0, contract_addr), initial);
assert_eq!(ParachainBalances::free_balance(BOB), initial + amount);
assert_eq!(ParachainBalances::free_balance(&contract_addr), initial - amount);
});
}

#[test]
fn test_xcm_execute_incomplete() {
MockNet::reset();

let contract_addr = instantiate_test_contract("xcm_execute");
let amount = 10 * CENTS;

// Execute XCM instructions through the contract.
ParaA::execute_with(|| {
// The XCM used to transfer funds to Bob.
let message: Xcm<()> = Xcm(vec![
WithdrawAsset(vec![(Here, amount).into()].into()),
// This will fail as the contract does not have enough balance to complete both
// withdrawals.
WithdrawAsset(vec![(Here, INITIAL_BALANCE).into()].into()),
DepositAsset {
assets: All.into(),
beneficiary: AccountId32 { network: None, id: BOB.clone().into() }.into(),
},
]);

let result = ParachainContracts::bare_call(
ALICE,
contract_addr.clone(),
0,
Weight::MAX,
None,
VersionedXcm::V4(message).encode(),
DebugInfo::UnsafeDebug,
CollectEvents::UnsafeCollect,
Determinism::Enforced,
);

assert_eq!(result.gas_consumed, result.gas_required);
assert_return_code!(&result.result.unwrap(), ReturnErrorCode::XcmExecutionFailed);

assert_eq!(ParachainBalances::free_balance(BOB), INITIAL_BALANCE);
assert_eq!(ParachainBalances::free_balance(&contract_addr), INITIAL_BALANCE - amount);
});
}

Expand Down Expand Up @@ -182,17 +226,10 @@ fn test_xcm_execute_reentrant_call() {
DebugInfo::UnsafeDebug,
CollectEvents::UnsafeCollect,
Determinism::Enforced,
)
.result
.unwrap();

let mut data = &result.data[..];
let outcome = Outcome::decode(&mut data).expect("Failed to decode xcm_execute Outcome");
assert_matches!(
outcome,
Outcome::Incomplete { used: _, error: XcmError::ExpectationFalse }
);

assert_return_code!(&result.result.unwrap(), ReturnErrorCode::XcmExecutionFailed);

// Funds should not change hands as the XCM transact failed.
assert_eq!(ParachainBalances::free_balance(BOB), INITIAL_BALANCE);
});
Expand Down
7 changes: 7 additions & 0 deletions substrate/frame/contracts/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,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;

/// Returns `true` if debug message recording is enabled. Otherwise `false` is returned.
fn debug_buffer_enabled(&self) -> bool;

/// Call some dispatchable and return the result.
fn call_runtime(&self, call: <Self::T as Config>::RuntimeCall) -> DispatchResultWithPostInfo;

Expand Down Expand Up @@ -1429,6 +1432,10 @@ where
self.top_frame_mut().nested_storage.charge(diff)
}

fn debug_buffer_enabled(&self) -> bool {
self.debug_message.is_some()
}

fn append_debug_buffer(&mut self, msg: &str) -> bool {
if let Some(buffer) = &mut self.debug_message {
buffer
Expand Down
9 changes: 8 additions & 1 deletion substrate/frame/contracts/src/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ pub trait Token<T: Config>: Copy + Clone + TestAuxiliaries {
/// while calculating the amount. In this case it is ok to use saturating operations
/// since on overflow they will return `max_value` which should consume all gas.
fn weight(&self) -> Weight;

/// Returns true if this token is expected to influence the lowest gas limit.
fn influence_lowest_gas_limit(&self) -> bool {
true
}
}

/// A wrapper around a type-erased trait object of what used to be a `Token`.
Expand Down Expand Up @@ -160,7 +165,9 @@ impl<T: Config> GasMeter<T> {
/// 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();
if token.influence_lowest_gas_limit() {
self.gas_left_lowest = self.gas_left_lowest();
}
let adjustment = charged_amount.0.saturating_sub(token.weight());
self.gas_left = self.gas_left.saturating_add(adjustment).min(self.gas_limit);
}
Expand Down
4 changes: 4 additions & 0 deletions substrate/frame/contracts/src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,10 @@ mod tests {
&mut self.gas_meter
}
fn charge_storage(&mut self, _diff: &crate::storage::meter::Diff) {}

fn debug_buffer_enabled(&self) -> bool {
true
}
fn append_debug_buffer(&mut self, msg: &str) -> bool {
self.debug_buffer.extend(msg.as_bytes());
true
Expand Down
95 changes: 41 additions & 54 deletions substrate/frame/contracts/src/wasm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use crate::{
exec::{ExecError, ExecResult, Ext, Key, TopicOf},
gas::{ChargedAmount, Token},
primitives::ExecReturnValue,
schedule::HostFnWeights,
BalanceOf, CodeHash, Config, DebugBufferVec, Error, SENTINEL,
};
use codec::{Decode, DecodeLimit, Encode, MaxEncodedLen};
Expand Down Expand Up @@ -235,6 +234,8 @@ pub enum RuntimeCosts {
ChainExtension(Weight),
/// Weight charged for calling into the runtime.
CallRuntime(Weight),
/// Weight charged for calling xcm_execute.
CallXcmExecute(Weight),
/// Weight of calling `seal_set_code_hash`
SetCodeHash,
/// Weight of calling `ecdsa_to_eth_address`
Expand All @@ -251,10 +252,18 @@ pub enum RuntimeCosts {
RemoveDelegateDependency,
}

impl RuntimeCosts {
fn token<T: Config>(&self, s: &HostFnWeights<T>) -> RuntimeToken {
impl<T: Config> Token<T> for RuntimeCosts {
fn influence_lowest_gas_limit(&self) -> bool {
match self {
&Self::CallXcmExecute(_) => false,
_ => true,
}
}

fn weight(&self) -> Weight {
let s = T::Schedule::get().host_fn_weights;
use self::RuntimeCosts::*;
let weight = match *self {
match *self {
CopyFromContract(len) => s.return_per_byte.saturating_mul(len.into()),
CopyToContract(len) => s.input_per_byte.saturating_mul(len.into()),
Caller => s.caller,
Expand Down Expand Up @@ -323,20 +332,14 @@ impl RuntimeCosts {
Sr25519Verify(len) => s
.sr25519_verify
.saturating_add(s.sr25519_verify_per_byte.saturating_mul(len.into())),
ChainExtension(weight) => weight,
CallRuntime(weight) => weight,
ChainExtension(weight) | CallRuntime(weight) | CallXcmExecute(weight) => weight,
SetCodeHash => s.set_code_hash,
EcdsaToEthAddress => s.ecdsa_to_eth_address,
ReentrantCount => s.reentrance_count,
AccountEntranceCount => s.account_reentrance_count,
InstantationNonce => s.instantiation_nonce,
AddDelegateDependency => s.add_delegate_dependency,
RemoveDelegateDependency => s.remove_delegate_dependency,
};
RuntimeToken {
#[cfg(test)]
_created_from: *self,
weight,
}
}
}
Expand All @@ -347,25 +350,10 @@ impl RuntimeCosts {
/// a function won't work out.
macro_rules! charge_gas {
($runtime:expr, $costs:expr) => {{
let token = $costs.token(&$runtime.ext.schedule().host_fn_weights);
$runtime.ext.gas_meter_mut().charge(token)
$runtime.ext.gas_meter_mut().charge($costs)
}};
}

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
#[derive(Copy, Clone)]
struct RuntimeToken {
#[cfg(test)]
_created_from: RuntimeCosts,
weight: Weight,
}

impl<T: Config> Token<T> for RuntimeToken {
fn weight(&self) -> Weight {
self.weight
}
}

/// The kind of call that should be performed.
enum CallType {
/// Execute another instantiated contract
Expand Down Expand Up @@ -506,28 +494,25 @@ impl<'a, E: Ext + 'a> Runtime<'a, E> {
/// 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_mut().adjust_gas(charged, token);
self.ext.gas_meter_mut().adjust_gas(charged, actual_costs);
}

/// Charge, Run and adjust gas, for executing the given dispatchable.
fn call_dispatchable<
ErrorReturnCode: Get<ReturnErrorCode>,
F: FnOnce(&mut Self) -> DispatchResultWithPostInfo,
>(
fn call_dispatchable<ErrorReturnCode: Get<ReturnErrorCode>>(
&mut self,
dispatch_info: DispatchInfo,
run: F,
runtime_cost: impl Fn(Weight) -> RuntimeCosts,
run: impl FnOnce(&mut Self) -> DispatchResultWithPostInfo,
) -> Result<ReturnErrorCode, TrapReason> {
use frame_support::dispatch::extract_actual_weight;
let charged = self.charge_gas(RuntimeCosts::CallRuntime(dispatch_info.weight))?;
let charged = self.charge_gas(runtime_cost(dispatch_info.weight))?;
let result = run(self);
let actual_weight = extract_actual_weight(&result, &dispatch_info);
self.adjust_gas(charged, RuntimeCosts::CallRuntime(actual_weight));
self.adjust_gas(charged, runtime_cost(actual_weight));
match result {
Ok(_) => Ok(ReturnErrorCode::Success),
Err(e) => {
if self.ext.append_debug_buffer("") {
if self.ext.debug_buffer_enabled() {
self.ext.append_debug_buffer("call failed with: ");
self.ext.append_debug_buffer(e.into());
};
Expand Down Expand Up @@ -2113,9 +2098,11 @@ pub mod env {
ctx.charge_gas(RuntimeCosts::CopyFromContract(call_len))?;
let call: <E::T as Config>::RuntimeCall =
ctx.read_sandbox_memory_as_unbounded(memory, call_ptr, call_len)?;
ctx.call_dispatchable::<CallRuntimeFailed, _>(call.get_dispatch_info(), |ctx| {
ctx.ext.call_runtime(call)
})
ctx.call_dispatchable::<CallRuntimeFailed>(
call.get_dispatch_info(),
RuntimeCosts::CallRuntime,
|ctx| ctx.ext.call_runtime(call),
)
}

/// Execute an XCM program locally, using the contract's address as the origin.
Expand All @@ -2126,7 +2113,6 @@ pub mod env {
memory: _,
msg_ptr: u32,
msg_len: u32,
output_ptr: u32,
) -> Result<ReturnErrorCode, TrapReason> {
use frame_support::dispatch::DispatchInfo;
use xcm::VersionedXcm;
Expand All @@ -2135,26 +2121,27 @@ pub mod env {
ctx.charge_gas(RuntimeCosts::CopyFromContract(msg_len))?;
let message: VersionedXcm<CallOf<E::T>> =
ctx.read_sandbox_memory_as_unbounded(memory, msg_ptr, msg_len)?;
ensure_executable::<E::T>(&message)?;

let execute_weight =
<<E::T as Config>::Xcm as ExecuteController<_, _>>::WeightInfo::execute();
let weight = ctx.ext.gas_meter().gas_left().max(execute_weight);
let dispatch_info = DispatchInfo { weight, ..Default::default() };

ensure_executable::<E::T>(&message)?;
ctx.call_dispatchable::<XcmExecutionFailed, _>(dispatch_info, |ctx| {
let origin = crate::RawOrigin::Signed(ctx.ext.address().clone()).into();
let outcome = <<E::T as Config>::Xcm>::execute(
origin,
Box::new(message),
weight.saturating_sub(execute_weight),
)?;
ctx.call_dispatchable::<XcmExecutionFailed>(
dispatch_info,
RuntimeCosts::CallXcmExecute,
|ctx| {
let origin = crate::RawOrigin::Signed(ctx.ext.address().clone()).into();
let weight_used = <<E::T as Config>::Xcm>::execute(
origin,
Box::new(message),
weight.saturating_sub(execute_weight),
)?;

ctx.write_sandbox_memory(memory, output_ptr, &outcome.encode())?;
let pre_dispatch_weight =
<<E::T as Config>::Xcm as ExecuteController<_, _>>::WeightInfo::execute();
Ok(Some(outcome.weight_used().saturating_add(pre_dispatch_weight)).into())
})
Ok(Some(weight_used.saturating_add(execute_weight)).into())
},
)
}

/// Send an XCM program from the contract to the specified destination.
Expand Down
3 changes: 1 addition & 2 deletions substrate/frame/contracts/uapi/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ pub trait HostFn {
#[deprecated(
note = "Unstable function. Behaviour can change without further notice. Use only for testing."
)]
fn xcm_execute(msg: &[u8], output: &mut &mut [u8]) -> Result;
fn xcm_execute(msg: &[u8]) -> Result;

/// Send an XCM program from the contract to the specified destination.
/// This is equivalent to dispatching `pallet_xcm::send` through `call_runtime`, except that
Expand All @@ -804,7 +804,6 @@ pub trait HostFn {
/// traps otherwise.
/// - `msg`: The message, should be decodable as a [VersionedXcm](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/enum.VersionedXcm.html),
/// traps otherwise.
/// - `output`: A reference to the output data buffer to write the [XcmHash](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/v3/type.XcmHash.html)
///
/// # Return
///
Expand Down
Loading

0 comments on commit 5987222

Please sign in to comment.