mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 15:11:02 +00:00
seal: Fix and improve error reporting (#6773)
* seal: Rework ext_transfer, ext_instantiate, ext_call error handling * Deny calling plain accounts (must use transfer now) * Return proper module error rather than ad-hoc strings * Return the correct error codes from call,instantiate (documentation was wrong) * Make ext_transfer fallible again to make it consistent with ext_call * seal: Improve error messages on memory access failures * seal: Convert contract trapped to module error * seal: Add additional tests for transfer, call, instantiate These tests verify that those functions return the error types which are declared in its docs. * Make it more pronounced that to_execution_result handles trap_reason * Improve ReturnCode docs * Fix whitespace issues in wat files * Improve ReturnCode doc * Improve ErrorOrigin doc and variant naming * Improve docs on ExecResult and ExecError * Encode u32 sentinel value as hex * with_nested_context no longer accepts an Option for trie * Fix successful typo * Rename InvalidContractCalled to NotCallable
This commit is contained in:
committed by
GitHub
parent
0553dabe32
commit
6671d017d6
@@ -18,7 +18,7 @@
|
||||
|
||||
use crate::{Schedule, Trait, CodeHash, BalanceOf, Error};
|
||||
use crate::exec::{
|
||||
Ext, ExecResult, ExecReturnValue, StorageKey, TopicOf, ReturnFlags,
|
||||
Ext, ExecResult, ExecReturnValue, StorageKey, TopicOf, ReturnFlags, ExecError
|
||||
};
|
||||
use crate::gas::{Gas, GasMeter, Token, GasMeterResult};
|
||||
use crate::wasm::env_def::ConvertibleToWasm;
|
||||
@@ -36,21 +36,33 @@ use sp_io::hashing::{
|
||||
sha2_256,
|
||||
};
|
||||
|
||||
/// Every error that can be returned from a runtime API call.
|
||||
/// Every error that can be returned to a contract when it calls any of the host functions.
|
||||
#[repr(u32)]
|
||||
pub enum ReturnCode {
|
||||
/// API call successful.
|
||||
Success = 0,
|
||||
/// The called function trapped and has its state changes reverted.
|
||||
/// In this case no output buffer is returned.
|
||||
/// Can only be returned from `ext_call` and `ext_instantiate`.
|
||||
CalleeTrapped = 1,
|
||||
/// The called function ran to completion but decided to revert its state.
|
||||
/// An output buffer is returned when one was supplied.
|
||||
/// Can only be returned from `ext_call` and `ext_instantiate`.
|
||||
CalleeReverted = 2,
|
||||
/// The passed key does not exist in storage.
|
||||
KeyNotFound = 3,
|
||||
/// Transfer failed because it would have brought the sender's total balance below the
|
||||
/// subsistence threshold.
|
||||
BelowSubsistenceThreshold = 4,
|
||||
/// Transfer failed for other reasons. Most probably reserved or locked balance of the
|
||||
/// sender prevents the transfer.
|
||||
TransferFailed = 5,
|
||||
/// The newly created contract is below the subsistence threshold after executing
|
||||
/// its constructor.
|
||||
NewContractNotFunded = 6,
|
||||
/// No code could be found at the supplied code hash.
|
||||
CodeNotFound = 7,
|
||||
/// The contract that was called is either no contract at all (a plain account)
|
||||
/// or is a tombstone.
|
||||
NotCallable = 8,
|
||||
}
|
||||
|
||||
impl ConvertibleToWasm for ReturnCode {
|
||||
@@ -66,7 +78,7 @@ impl ConvertibleToWasm for ReturnCode {
|
||||
}
|
||||
|
||||
impl From<ExecReturnValue> for ReturnCode {
|
||||
fn from(from: ExecReturnValue) -> ReturnCode {
|
||||
fn from(from: ExecReturnValue) -> Self {
|
||||
if from.flags.contains(ReturnFlags::REVERT) {
|
||||
Self::CalleeReverted
|
||||
} else {
|
||||
@@ -96,7 +108,7 @@ enum TrapReason {
|
||||
SupervisorError(DispatchError),
|
||||
/// Signals that trap was generated in response to call `ext_return` host function.
|
||||
Return(ReturnData),
|
||||
/// Signals that a trap was generated in response to a succesful call to the
|
||||
/// Signals that a trap was generated in response to a successful call to the
|
||||
/// `ext_terminate` host function.
|
||||
Termination,
|
||||
/// Signals that a trap was generated because of a successful restoration.
|
||||
@@ -131,35 +143,42 @@ impl<'a, E: Ext + 'a> Runtime<'a, E> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the sandbox result and the runtime state into the execution outcome.
|
||||
///
|
||||
/// It evaluates information stored in the `trap_reason` variable of the runtime and
|
||||
/// bases the outcome on the value if this variable. Only if `trap_reason` is `None`
|
||||
/// the result of the sandbox is evaluated.
|
||||
pub(crate) fn to_execution_result<E: Ext>(
|
||||
runtime: Runtime<E>,
|
||||
sandbox_result: Result<sp_sandbox::ReturnValue, sp_sandbox::Error>,
|
||||
) -> ExecResult {
|
||||
match runtime.trap_reason {
|
||||
// The trap was the result of the execution `return` host function.
|
||||
Some(TrapReason::Return(ReturnData{ flags, data })) => {
|
||||
let flags = ReturnFlags::from_bits(flags).ok_or_else(||
|
||||
"used reserved bit in return flags"
|
||||
)?;
|
||||
return Ok(ExecReturnValue {
|
||||
flags,
|
||||
data,
|
||||
})
|
||||
},
|
||||
Some(TrapReason::Termination) => {
|
||||
return Ok(ExecReturnValue {
|
||||
flags: ReturnFlags::empty(),
|
||||
data: Vec::new(),
|
||||
})
|
||||
},
|
||||
Some(TrapReason::Restoration) => {
|
||||
return Ok(ExecReturnValue {
|
||||
flags: ReturnFlags::empty(),
|
||||
data: Vec::new(),
|
||||
})
|
||||
// If a trap reason is set we base our decision solely on that.
|
||||
if let Some(trap_reason) = runtime.trap_reason {
|
||||
return match trap_reason {
|
||||
// The trap was the result of the execution `return` host function.
|
||||
TrapReason::Return(ReturnData{ flags, data }) => {
|
||||
let flags = ReturnFlags::from_bits(flags).ok_or_else(||
|
||||
"used reserved bit in return flags"
|
||||
)?;
|
||||
Ok(ExecReturnValue {
|
||||
flags,
|
||||
data,
|
||||
})
|
||||
},
|
||||
TrapReason::Termination => {
|
||||
Ok(ExecReturnValue {
|
||||
flags: ReturnFlags::empty(),
|
||||
data: Vec::new(),
|
||||
})
|
||||
},
|
||||
TrapReason::Restoration => {
|
||||
Ok(ExecReturnValue {
|
||||
flags: ReturnFlags::empty(),
|
||||
data: Vec::new(),
|
||||
})
|
||||
},
|
||||
TrapReason::SupervisorError(error) => Err(error)?,
|
||||
}
|
||||
Some(TrapReason::SupervisorError(error)) => Err(error)?,
|
||||
None => (),
|
||||
}
|
||||
|
||||
// Check the exact type of the error.
|
||||
@@ -178,7 +197,7 @@ pub(crate) fn to_execution_result<E: Ext>(
|
||||
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("contract trapped during execution")?,
|
||||
Err(Error::<E::T>::ContractTrapped)?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +299,8 @@ fn read_sandbox_memory<E: Ext>(
|
||||
)?;
|
||||
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
ctx.memory.get(ptr, buf.as_mut_slice()).map_err(|_| sp_sandbox::HostError)?;
|
||||
ctx.memory.get(ptr, buf.as_mut_slice())
|
||||
.map_err(|_| store_err(ctx, Error::<E::T>::OutOfBounds))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
@@ -304,7 +324,7 @@ fn read_sandbox_memory_into_buf<E: Ext>(
|
||||
RuntimeToken::ReadMemory(buf.len() as u32),
|
||||
)?;
|
||||
|
||||
ctx.memory.get(ptr, buf).map_err(Into::into)
|
||||
ctx.memory.get(ptr, buf).map_err(|_| store_err(ctx, Error::<E::T>::OutOfBounds))
|
||||
}
|
||||
|
||||
/// Read designated chunk from the sandbox memory, consuming an appropriate amount of
|
||||
@@ -322,7 +342,7 @@ fn read_sandbox_memory_as<E: Ext, D: Decode>(
|
||||
len: u32,
|
||||
) -> Result<D, sp_sandbox::HostError> {
|
||||
let buf = read_sandbox_memory(ctx, ptr, len)?;
|
||||
D::decode(&mut &buf[..]).map_err(|_| sp_sandbox::HostError)
|
||||
D::decode(&mut &buf[..]).map_err(|_| store_err(ctx, Error::<E::T>::DecodingFailed))
|
||||
}
|
||||
|
||||
/// Write the given buffer to the designated location in the sandbox memory, consuming
|
||||
@@ -345,9 +365,8 @@ fn write_sandbox_memory<E: Ext>(
|
||||
RuntimeToken::WriteMemory(buf.len() as u32),
|
||||
)?;
|
||||
|
||||
ctx.memory.set(ptr, buf)?;
|
||||
|
||||
Ok(())
|
||||
ctx.memory.set(ptr, buf)
|
||||
.map_err(|_| store_err(ctx, Error::<E::T>::OutOfBounds))
|
||||
}
|
||||
|
||||
/// Write the given buffer and its length to the designated locations in sandbox memory.
|
||||
@@ -379,7 +398,7 @@ fn write_sandbox_output<E: Ext>(
|
||||
let len: u32 = read_sandbox_memory_as(ctx, out_len_ptr, 4)?;
|
||||
|
||||
if len < buf_len {
|
||||
Err(map_err(ctx, Error::<E::T>::OutputBufferTooSmall))?
|
||||
Err(store_err(ctx, Error::<E::T>::OutputBufferTooSmall))?
|
||||
}
|
||||
|
||||
charge_gas(
|
||||
@@ -398,7 +417,7 @@ fn write_sandbox_output<E: Ext>(
|
||||
/// Stores a DispatchError returned from an Ext function into the trap_reason.
|
||||
///
|
||||
/// This allows through supervisor generated errors to the caller.
|
||||
fn map_err<E, Error>(ctx: &mut Runtime<E>, err: Error) -> sp_sandbox::HostError where
|
||||
fn store_err<E, Error>(ctx: &mut Runtime<E>, err: Error) -> sp_sandbox::HostError where
|
||||
E: Ext,
|
||||
Error: Into<DispatchError>,
|
||||
{
|
||||
@@ -406,12 +425,86 @@ fn map_err<E, Error>(ctx: &mut Runtime<E>, err: Error) -> sp_sandbox::HostError
|
||||
sp_sandbox::HostError
|
||||
}
|
||||
|
||||
/// Fallible conversion of `DispatchError` to `ReturnCode`.
|
||||
fn err_into_return_code<T: Trait>(from: DispatchError) -> Result<ReturnCode, DispatchError> {
|
||||
use ReturnCode::*;
|
||||
|
||||
let below_sub = Error::<T>::BelowSubsistenceThreshold.into();
|
||||
let transfer_failed = Error::<T>::TransferFailed.into();
|
||||
let not_funded = Error::<T>::NewContractNotFunded.into();
|
||||
let no_code = Error::<T>::CodeNotFound.into();
|
||||
let invalid_contract = Error::<T>::NotCallable.into();
|
||||
|
||||
match from {
|
||||
x if x == below_sub => Ok(BelowSubsistenceThreshold),
|
||||
x if x == transfer_failed => Ok(TransferFailed),
|
||||
x if x == not_funded => Ok(NewContractNotFunded),
|
||||
x if x == no_code => Ok(CodeNotFound),
|
||||
x if x == invalid_contract => Ok(NotCallable),
|
||||
err => Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallible conversion of a `ExecResult` to `ReturnCode`.
|
||||
fn exec_into_return_code<T: Trait>(from: ExecResult) -> Result<ReturnCode, DispatchError> {
|
||||
use crate::exec::ErrorOrigin::Callee;
|
||||
|
||||
let ExecError { error, origin } = match from {
|
||||
Ok(retval) => return Ok(retval.into()),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
match (error, origin) {
|
||||
(_, Callee) => Ok(ReturnCode::CalleeTrapped),
|
||||
(err, _) => err_into_return_code::<T>(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by Runtime API that calls into other contracts.
|
||||
///
|
||||
/// Those need to transform the the `ExecResult` returned from the execution into
|
||||
/// a `ReturnCode`. If this conversion fails because the `ExecResult` constitutes a
|
||||
/// a fatal error then this error is stored in the `ExecutionContext` so it can be
|
||||
/// extracted for display in the UI.
|
||||
fn map_exec_result<E: Ext>(ctx: &mut Runtime<E>, result: ExecResult)
|
||||
-> Result<ReturnCode, sp_sandbox::HostError>
|
||||
{
|
||||
match exec_into_return_code::<E::T>(result) {
|
||||
Ok(code) => Ok(code),
|
||||
Err(err) => Err(store_err(ctx, err)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert an error into a `ReturnCode`.
|
||||
///
|
||||
/// Used to decide between fatal and non-fatal errors.
|
||||
fn map_dispatch_result<T, E: Ext>(ctx: &mut Runtime<E>, result: Result<T, DispatchError>)
|
||||
-> Result<ReturnCode, sp_sandbox::HostError>
|
||||
{
|
||||
let err = if let Err(err) = result {
|
||||
err
|
||||
} else {
|
||||
return Ok(ReturnCode::Success)
|
||||
};
|
||||
|
||||
match err_into_return_code::<E::T>(err) {
|
||||
Ok(code) => Ok(code),
|
||||
Err(err) => Err(store_err(ctx, err)),
|
||||
}
|
||||
}
|
||||
|
||||
// ***********************************************************
|
||||
// * AFTER MAKING A CHANGE MAKE SURE TO UPDATE COMPLEXITY.MD *
|
||||
// ***********************************************************
|
||||
|
||||
// Define a function `fn init_env<E: Ext>() -> HostFunctionSet<E>` that returns
|
||||
// a function set which can be imported by an executed contract.
|
||||
//
|
||||
// # Note
|
||||
//
|
||||
// Any input that leads to a out of bound error (reading or writing) or failing to decode
|
||||
// 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.
|
||||
@@ -441,7 +534,7 @@ define_env!(Env, <E: Ext>,
|
||||
// - `value_ptr`: pointer into the linear memory where the value to set is placed.
|
||||
// - `value_len`: the length of the value in bytes.
|
||||
//
|
||||
// # Errors
|
||||
// # Traps
|
||||
//
|
||||
// - If value length exceeds the configured maximum value length of a storage entry.
|
||||
// - Upon trying to set an empty storage entry (value length is 0).
|
||||
@@ -480,12 +573,7 @@ define_env!(Env, <E: Ext>,
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// If there is no entry under the given key then this function will return
|
||||
// `ReturnCode::KeyNotFound`.
|
||||
//
|
||||
// # Traps
|
||||
//
|
||||
// Traps if the supplied buffer length is smaller than the size of the stored value.
|
||||
// `ReturnCode::KeyNotFound`
|
||||
ext_get_storage(ctx, key_ptr: u32, out_ptr: u32, out_len_ptr: u32) -> ReturnCode => {
|
||||
let mut key: StorageKey = [0; 32];
|
||||
read_sandbox_memory_into_buf(ctx, key_ptr, &mut key)?;
|
||||
@@ -508,24 +596,24 @@ define_env!(Env, <E: Ext>,
|
||||
// Should be decodable as a `T::Balance`. Traps otherwise.
|
||||
// - value_len: length of the value buffer.
|
||||
//
|
||||
// # Traps
|
||||
// # Errors
|
||||
//
|
||||
// Traps if the transfer wasn't succesful. This can happen when the value transfered
|
||||
// brings the sender below the existential deposit. Use `ext_terminate` to remove
|
||||
// the caller contract.
|
||||
// `ReturnCode::BelowSubsistenceThreshold`
|
||||
// `ReturnCode::TransferFailed`
|
||||
ext_transfer(
|
||||
ctx,
|
||||
account_ptr: u32,
|
||||
account_len: u32,
|
||||
value_ptr: u32,
|
||||
value_len: u32
|
||||
) => {
|
||||
) -> ReturnCode => {
|
||||
let callee: <<E as Ext>::T as frame_system::Trait>::AccountId =
|
||||
read_sandbox_memory_as(ctx, account_ptr, account_len)?;
|
||||
let value: BalanceOf<<E as Ext>::T> =
|
||||
read_sandbox_memory_as(ctx, value_ptr, value_len)?;
|
||||
|
||||
ctx.ext.transfer(&callee, value, ctx.gas_meter).map_err(|e| map_err(ctx, e))
|
||||
let result = ctx.ext.transfer(&callee, value, ctx.gas_meter);
|
||||
map_dispatch_result(ctx, result)
|
||||
},
|
||||
|
||||
// Make a call to another contract.
|
||||
@@ -551,17 +639,14 @@ define_env!(Env, <E: Ext>,
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// `ReturnCode::CalleeReverted`: The callee ran to completion but decided to have its
|
||||
// changes reverted. The delivery of the output buffer is still possible.
|
||||
// `ReturnCode::CalleeTrapped`: The callee trapped during execution. All changes are reverted
|
||||
// and no output buffer is delivered.
|
||||
// An error means that the call wasn't successful output buffer is returned unless
|
||||
// stated otherwise.
|
||||
//
|
||||
// # Traps
|
||||
//
|
||||
// - Transfer of balance failed. This call can not bring the sender below the existential
|
||||
// deposit. Use `ext_terminate` to remove the caller.
|
||||
// - Callee does not exist.
|
||||
// - Supplied output buffer is too small.
|
||||
// `ReturnCode::CalleeReverted`: Output buffer is returned.
|
||||
// `ReturnCode::CalleeTrapped`
|
||||
// `ReturnCode::BelowSubsistenceThreshold`
|
||||
// `ReturnCode::TransferFailed`
|
||||
// `ReturnCode::NotCallable`
|
||||
ext_call(
|
||||
ctx,
|
||||
callee_ptr: u32,
|
||||
@@ -594,22 +679,16 @@ define_env!(Env, <E: Ext>,
|
||||
nested_meter,
|
||||
input_data,
|
||||
)
|
||||
.map_err(|_| ())
|
||||
}
|
||||
// there is not enough gas to allocate for the nested call.
|
||||
None => Err(()),
|
||||
None => Err(Error::<<E as Ext>::T>::OutOfGas.into()),
|
||||
}
|
||||
});
|
||||
|
||||
match call_outcome {
|
||||
Ok(output) => {
|
||||
write_sandbox_output(ctx, output_ptr, output_len_ptr, &output.data, true)?;
|
||||
Ok(output.into())
|
||||
},
|
||||
Err(_) => {
|
||||
Ok(ReturnCode::CalleeTrapped)
|
||||
},
|
||||
if let Ok(output) = &call_outcome {
|
||||
write_sandbox_output(ctx, output_ptr, output_len_ptr, &output.data, true)?;
|
||||
}
|
||||
map_exec_result(ctx, call_outcome)
|
||||
},
|
||||
|
||||
// Instantiate a contract with the specified code hash.
|
||||
@@ -643,19 +722,18 @@ define_env!(Env, <E: Ext>,
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// `ReturnCode::CalleeReverted`: The callee's constructor ran to completion but decided to have
|
||||
// its changes reverted. The delivery of the output buffer is still possible but the
|
||||
// account was not created and no address is returned.
|
||||
// `ReturnCode::CalleeTrapped`: The callee trapped during execution. All changes are reverted
|
||||
// and no output buffer is delivered. The accounts was not created and no address is
|
||||
// returned.
|
||||
// Please consult the `ReturnCode` enum declaration for more information on those
|
||||
// errors. Here we only note things specific to this function.
|
||||
//
|
||||
// # Traps
|
||||
// An error means that the account wasn't created and no address or output buffer
|
||||
// is returned unless stated otherwise.
|
||||
//
|
||||
// - Transfer of balance failed. This call can not bring the sender below the existential
|
||||
// deposit. Use `ext_terminate` to remove the caller.
|
||||
// - Code hash does not exist.
|
||||
// - Supplied output buffers are too small.
|
||||
// `ReturnCode::CalleeReverted`: Output buffer is returned.
|
||||
// `ReturnCode::CalleeTrapped`
|
||||
// `ReturnCode::BelowSubsistenceThreshold`
|
||||
// `ReturnCode::TransferFailed`
|
||||
// `ReturnCode::NewContractNotFunded`
|
||||
// `ReturnCode::CodeNotFound`
|
||||
ext_instantiate(
|
||||
ctx,
|
||||
code_hash_ptr: u32,
|
||||
@@ -690,26 +768,20 @@ define_env!(Env, <E: Ext>,
|
||||
nested_meter,
|
||||
input_data
|
||||
)
|
||||
.map_err(|_| ())
|
||||
}
|
||||
// there is not enough gas to allocate for the nested call.
|
||||
None => Err(()),
|
||||
None => Err(Error::<<E as Ext>::T>::OutOfGas.into()),
|
||||
}
|
||||
});
|
||||
match instantiate_outcome {
|
||||
Ok((address, output)) => {
|
||||
if !output.flags.contains(ReturnFlags::REVERT) {
|
||||
write_sandbox_output(
|
||||
ctx, address_ptr, address_len_ptr, &address.encode(), true
|
||||
)?;
|
||||
}
|
||||
write_sandbox_output(ctx, output_ptr, output_len_ptr, &output.data, true)?;
|
||||
Ok(output.into())
|
||||
},
|
||||
Err(_) => {
|
||||
Ok(ReturnCode::CalleeTrapped)
|
||||
},
|
||||
if let Ok((address, output)) = &instantiate_outcome {
|
||||
if !output.flags.contains(ReturnFlags::REVERT) {
|
||||
write_sandbox_output(
|
||||
ctx, address_ptr, address_len_ptr, &address.encode(), true
|
||||
)?;
|
||||
}
|
||||
write_sandbox_output(ctx, output_ptr, output_len_ptr, &output.data, true)?;
|
||||
}
|
||||
map_exec_result(ctx, instantiate_outcome.map(|(_id, retval)| retval))
|
||||
},
|
||||
|
||||
// Remove the calling account and transfer remaining balance.
|
||||
@@ -722,6 +794,10 @@ define_env!(Env, <E: Ext>,
|
||||
// where all remaining funds of the caller are transfered.
|
||||
// Should be decodable as an `T::AccountId`. Traps otherwise.
|
||||
// - beneficiary_len: length of the address buffer.
|
||||
//
|
||||
// # Traps
|
||||
//
|
||||
// - The contract is live i.e is already on the call stack.
|
||||
ext_terminate(
|
||||
ctx,
|
||||
beneficiary_ptr: u32,
|
||||
@@ -939,6 +1015,11 @@ define_env!(Env, <E: Ext>,
|
||||
// encodes the rent allowance that must be set in the case of successful restoration.
|
||||
// `delta_ptr` is the pointer to the start of a buffer that has `delta_count` storage keys
|
||||
// laid out sequentially.
|
||||
//
|
||||
// # Traps
|
||||
//
|
||||
// - Tombstone hashes do not match
|
||||
// - Calling cantract is live i.e is already on the call stack.
|
||||
ext_restore_to(
|
||||
ctx,
|
||||
dest_ptr: u32,
|
||||
|
||||
Reference in New Issue
Block a user