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:
Alexander Theißen
2020-08-03 12:03:22 +02:00
committed by GitHub
parent 0553dabe32
commit 6671d017d6
16 changed files with 855 additions and 265 deletions
+179 -98
View File
@@ -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,