contracts: Add new seal_call that offers new features (#8909)

* Add new `seal_call` that offers new features

* Fix doc typo

Co-authored-by: Michael Müller <michi@parity.io>

* Fix doc typos

Co-authored-by: Michael Müller <michi@parity.io>

* Fix comment on assert

* Update CHANGELOG.md

Co-authored-by: Michael Müller <michi@parity.io>
This commit is contained in:
Alexander Theißen
2021-06-07 19:40:23 +02:00
committed by GitHub
parent 5c14dd3f32
commit 60256d752e
7 changed files with 503 additions and 71 deletions
+150 -30
View File
@@ -24,6 +24,7 @@ use crate::{
wasm::env_def::ConvertibleToWasm,
schedule::HostFnWeights,
};
use bitflags::bitflags;
use pwasm_utils::parity_wasm::elements::ValueType;
use frame_support::{dispatch::DispatchError, ensure, traits::Get, weights::Weight};
use sp_std::prelude::*;
@@ -318,6 +319,47 @@ where
}
}
bitflags! {
/// Flags used to change the behaviour of `seal_call`.
struct CallFlags: u32 {
/// Forward the input of current function to the callee.
///
/// Supplied input pointers are ignored when set.
///
/// # Note
///
/// A forwarding call will consume the current contracts input. Any attempt to
/// access the input after this call returns will lead to [`Error::InputForwarded`].
/// It does not matter if this is due to calling `seal_input` or trying another
/// forwarding call. Consider using [`Self::CLONE_INPUT`] in order to preserve
/// the input.
const FORWARD_INPUT = 0b0000_0001;
/// Identical to [`Self::FORWARD_INPUT`] but without consuming the input.
///
/// This adds some additional weight costs to the call.
///
/// # Note
///
/// This implies [`Self::FORWARD_INPUT`] and takes precedence when both are set.
const CLONE_INPUT = 0b0000_0010;
/// Do not return from the call but rather return the result of the callee to the
/// callers caller.
///
/// # Note
///
/// This makes the current contract completely transparent to its caller by replacing
/// this contracts potential output by the callee ones. Any code after `seal_call`
/// can be safely considered unreachable.
const TAIL_CALL = 0b0000_0100;
/// Allow the callee to reenter into the current contract.
///
/// Without this flag any reentrancy into the current contract that originates from
/// the callee (or any of its callees) is denied. This includes the first callee:
/// You cannot call into yourself with this flag set.
const ALLOW_REENTRY = 0b0000_1000;
}
}
/// This is only appropriate when writing out data of constant size that does not depend on user
/// input. In this case the costs for this copy was already charged as part of the token at
/// the beginning of the API entry point.
@@ -402,8 +444,7 @@ where
//
// Because panics are really undesirable in the runtime code, we treat this as
// a trap for now. Eventually, we might want to revisit this.
Err(sp_sandbox::Error::Module) =>
Err("validation error")?,
Err(sp_sandbox::Error::Module) => Err("validation error")?,
// Any other kind of a trap should result in a failure.
Err(sp_sandbox::Error::Execution) | Err(sp_sandbox::Error::OutOfBounds) =>
Err(Error::<E::T>::ContractTrapped)?
@@ -629,6 +670,65 @@ where
(err, _) => Self::err_into_return_code(err)
}
}
fn call(
&mut self,
flags: CallFlags,
callee_ptr: u32,
callee_len: u32,
gas: u64,
value_ptr: u32,
value_len: u32,
input_data_ptr: u32,
input_data_len: u32,
output_ptr: u32,
output_len_ptr: u32
) -> Result<ReturnCode, TrapReason> {
self.charge_gas(RuntimeCosts::CallBase(input_data_len))?;
let callee: <<E as Ext>::T as frame_system::Config>::AccountId =
self.read_sandbox_memory_as(callee_ptr, callee_len)?;
let value: BalanceOf<<E as Ext>::T> = self.read_sandbox_memory_as(value_ptr, value_len)?;
let input_data = if flags.contains(CallFlags::CLONE_INPUT) {
self.input_data.as_ref().ok_or_else(|| Error::<E::T>::InputForwarded)?.clone()
} else if flags.contains(CallFlags::FORWARD_INPUT) {
self.input_data.take().ok_or_else(|| Error::<E::T>::InputForwarded)?
} else {
self.read_sandbox_memory(input_data_ptr, input_data_len)?
};
if value > 0u32.into() {
self.charge_gas(RuntimeCosts::CallSurchargeTransfer)?;
}
let charged = self.charge_gas(
RuntimeCosts::CallSurchargeCodeSize(<E::T as Config>::Schedule::get().limits.code_len)
)?;
let ext = &mut self.ext;
let call_outcome = ext.call(
gas, callee, value, input_data, flags.contains(CallFlags::ALLOW_REENTRY),
);
let code_len = match &call_outcome {
Ok((_, len)) => len,
Err((_, len)) => len,
};
self.adjust_gas(charged, RuntimeCosts::CallSurchargeCodeSize(*code_len));
// `TAIL_CALL` only matters on an `OK` result. Otherwise the call stack comes to
// a halt anyways without anymore code being executed.
if flags.contains(CallFlags::TAIL_CALL) {
if let Ok((return_value, _)) = call_outcome {
return Err(TrapReason::Return(ReturnData {
flags: return_value.flags.bits(),
data: return_value.data.0,
}));
}
}
if let Ok((output, _)) = &call_outcome {
self.write_sandbox_output(output_ptr, output_len_ptr, &output.data, true, |len| {
Some(RuntimeCosts::CallCopyOut(len))
})?;
}
Ok(Runtime::<E>::exec_into_return_code(call_outcome.map(|r| r.0).map_err(|r| r.0))?)
}
}
// ***********************************************************
@@ -758,6 +858,36 @@ define_env!(Env, <E: Ext>,
}
},
// Make a call to another contract.
//
// This is equivalent to calling the newer version of this function with
// `flags` set to `ALLOW_REENTRY`. See the newer version for documentation.
[seal0] seal_call(
ctx,
callee_ptr: u32,
callee_len: u32,
gas: u64,
value_ptr: u32,
value_len: u32,
input_data_ptr: u32,
input_data_len: u32,
output_ptr: u32,
output_len_ptr: u32
) -> ReturnCode => {
ctx.call(
CallFlags::ALLOW_REENTRY,
callee_ptr,
callee_len,
gas,
value_ptr,
value_len,
input_data_ptr,
input_data_len,
output_ptr,
output_len_ptr,
)
},
// Make a call to another contract.
//
// The callees output buffer is copied to `output_ptr` and its length to `output_len_ptr`.
@@ -766,6 +896,7 @@ define_env!(Env, <E: Ext>,
//
// # Parameters
//
// - flags: See [`CallFlags`] for a documenation of the supported flags.
// - callee_ptr: a pointer to the address of the callee contract.
// Should be decodable as an `T::AccountId`. Traps otherwise.
// - callee_len: length of the address buffer.
@@ -789,8 +920,9 @@ define_env!(Env, <E: Ext>,
// `ReturnCode::BelowSubsistenceThreshold`
// `ReturnCode::TransferFailed`
// `ReturnCode::NotCallable`
[seal0] seal_call(
[__unstable__] seal_call(
ctx,
flags: u32,
callee_ptr: u32,
callee_len: u32,
gas: u64,
@@ -801,30 +933,18 @@ define_env!(Env, <E: Ext>,
output_ptr: u32,
output_len_ptr: u32
) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::CallBase(input_data_len))?;
let callee: <<E as Ext>::T as frame_system::Config>::AccountId =
ctx.read_sandbox_memory_as(callee_ptr, callee_len)?;
let value: BalanceOf<<E as Ext>::T> = ctx.read_sandbox_memory_as(value_ptr, value_len)?;
let input_data = ctx.read_sandbox_memory(input_data_ptr, input_data_len)?;
if value > 0u32.into() {
ctx.charge_gas(RuntimeCosts::CallSurchargeTransfer)?;
}
let charged = ctx.charge_gas(
RuntimeCosts::CallSurchargeCodeSize(<E::T as Config>::Schedule::get().limits.code_len)
)?;
let ext = &mut ctx.ext;
let call_outcome = ext.call(gas, callee, value, input_data);
let code_len = match &call_outcome {
Ok((_, len)) => len,
Err((_, len)) => len,
};
ctx.adjust_gas(charged, RuntimeCosts::CallSurchargeCodeSize(*code_len));
if let Ok((output, _)) = &call_outcome {
ctx.write_sandbox_output(output_ptr, output_len_ptr, &output.data, true, |len| {
Some(RuntimeCosts::CallCopyOut(len))
})?;
}
Ok(Runtime::<E>::exec_into_return_code(call_outcome.map(|r| r.0).map_err(|r| r.0))?)
ctx.call(
CallFlags::from_bits(flags).ok_or_else(|| "used rerved bit in CallFlags")?,
callee_ptr,
callee_len,
gas,
value_ptr,
value_len,
input_data_ptr,
input_data_len,
output_ptr,
output_len_ptr,
)
},
// Instantiate a contract with the specified code hash.
@@ -945,7 +1065,6 @@ define_env!(Env, <E: Ext>,
ctx.charge_gas(RuntimeCosts::Terminate)?;
let beneficiary: <<E as Ext>::T as frame_system::Config>::AccountId =
ctx.read_sandbox_memory_as(beneficiary_ptr, beneficiary_len)?;
let charged = ctx.charge_gas(
RuntimeCosts::TerminateSurchargeCodeSize(
<E::T as Config>::Schedule::get().limits.code_len
@@ -969,16 +1088,17 @@ define_env!(Env, <E: Ext>,
//
// # Note
//
// This function can only be called once. Calling it multiple times will trigger a trap.
// This function traps if the input was previously forwarded by a `seal_call`.
[seal0] seal_input(ctx, out_ptr: u32, out_len_ptr: u32) => {
ctx.charge_gas(RuntimeCosts::InputBase)?;
if let Some(input) = ctx.input_data.take() {
ctx.write_sandbox_output(out_ptr, out_len_ptr, &input, false, |len| {
Some(RuntimeCosts::InputCopyOut(len))
})?;
ctx.input_data = Some(input);
Ok(())
} else {
Err(Error::<E::T>::InputAlreadyRead.into())
Err(Error::<E::T>::InputForwarded.into())
}
},