Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rework contract runtime error processing #4181

Merged
merged 19 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 44 additions & 4 deletions core/primitives/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::fmt::{Debug, Display};

use crate::hash::CryptoHash;
use near_rpc_error_macro::RpcError;
use near_vm_errors::{FunctionCallError, VMLogicError};
use near_vm_errors::{CompilationError, FunctionCallErrorSer, MethodResolveError, VMLogicError};

/// Error returned in the ExecutionOutcome in case of failure
#[derive(
Expand Down Expand Up @@ -332,6 +332,46 @@ pub struct ActionError {
pub kind: ActionErrorKind,
}

#[derive(Debug, Clone, PartialEq, Eq, RpcError)]
pub enum ContractCallError {
MethodResolveError(MethodResolveError),
CompilationError(CompilationError),
ExecutionError { msg: String },
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really good structure, self documented of what could be wrong in contract call :+!


impl From<ContractCallError> for FunctionCallErrorSer {
fn from(e: ContractCallError) -> Self {
match e {
ContractCallError::CompilationError(e) => FunctionCallErrorSer::CompilationError(e),
ContractCallError::MethodResolveError(e) => FunctionCallErrorSer::MethodResolveError(e),
ContractCallError::ExecutionError { msg } => FunctionCallErrorSer::ExecutionError(msg),
}
}
}

impl From<FunctionCallErrorSer> for ContractCallError {
fn from(e: FunctionCallErrorSer) -> Self {
match e {
FunctionCallErrorSer::CompilationError(e) => ContractCallError::CompilationError(e),
FunctionCallErrorSer::MethodResolveError(e) => ContractCallError::MethodResolveError(e),
FunctionCallErrorSer::ExecutionError(msg) => ContractCallError::ExecutionError { msg },
FunctionCallErrorSer::LinkError { msg } => ContractCallError::ExecutionError { msg },
FunctionCallErrorSer::WasmUnknownError => {
ContractCallError::ExecutionError { msg: "unknown error".to_string() }
}
FunctionCallErrorSer::EvmError(e) => {
ContractCallError::ExecutionError { msg: format!("EVM: {:?}", e) }
}
FunctionCallErrorSer::WasmTrap(e) => {
ContractCallError::ExecutionError { msg: format!("WASM: {:?}", e) }
}
FunctionCallErrorSer::HostError(e) => {
ContractCallError::ExecutionError { msg: format!("Host: {:?}", e) }
}
}
}
}

#[derive(
BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError,
)]
Expand Down Expand Up @@ -384,8 +424,8 @@ pub enum ActionErrorKind {
#[serde(with = "u128_dec_format")]
minimum_stake: Balance,
},
/// An error occurred during a `FunctionCall` Action.
FunctionCallError(FunctionCallError),
/// An error occurred during a `FunctionCall` Action, parameter is debug message.
FunctionCallError(FunctionCallErrorSer),
/// Error occurs when a new `ActionReceipt` created by the `FunctionCall` action fails
/// receipt validation.
NewReceiptValidationError(ReceiptValidationError),
Expand Down Expand Up @@ -689,7 +729,7 @@ impl Display for ActionErrorKind {
ActionErrorKind::DeleteAccountStaking { account_id } => {
write!(f, "Account {:?} is staking and can not be deleted", account_id)
}
ActionErrorKind::FunctionCallError(s) => write!(f, "{}", s),
ActionErrorKind::FunctionCallError(s) => write!(f, "{:?}", s),
ActionErrorKind::NewReceiptValidationError(e) => {
write!(f, "An new action receipt created during a FunctionCall is not valid: {}", e)
}
Expand Down
59 changes: 40 additions & 19 deletions runtime/near-vm-errors/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::fmt::{self, Error, Formatter};

use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};

use near_rpc_error_macro::RpcError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize)]
// TODO: remove serialization derives, once fix compilation caching.
#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
pub enum VMError {
FunctionCallError(FunctionCallError),
/// Serialized external error from External trait implementation.
Expand All @@ -17,10 +17,34 @@ pub enum VMError {
CacheError(CacheError),
}

#[derive(
Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError,
)]
// TODO(4217): remove serialization derives, once fix compilation caching.
#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
pub enum FunctionCallError {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this enum still needed given the presence of FunctionCallErrorSer and ContractCallError

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's used inside the contract runtime and have precise execution problem description. We don't want it to leak beyond contract runtime.

/// Wasm compilation error
CompilationError(CompilationError),
/// Wasm binary env link error
LinkError {
msg: String,
},
/// Import/export resolve error
MethodResolveError(MethodResolveError),
/// A trap happened during execution of a binary
WasmTrap(WasmTrap),
WasmUnknownError {
debug_message: String,
},
HostError(HostError),
EvmError(EvmError),
/// Non-deterministic error.
Nondeterministic(String),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this error deterministic, i.e, is it guaranteed that a nondeterministic error will be wrapped in this variant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, now it is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt of still keep it WasmUnkownError is a good for wasmer 1. The internal of it is really unknown in wasmer 0.x (UnknownTrap etc.) but it's indeed known in wasmer 1. I suggest name it as WasmerRuntimeError or WasmerInternalError

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworked, so that Nondeterministic is properly returned.

}

/// Serializable version of `FunctionCallError`. Must never reorder/remove elements, can only
/// add new variants at the end (but do that very carefully). This type must be never used
/// directly, and must be converted to `ContractCallError` instead using `into()` converter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One missing in doc: since it's never used directly, when it should be used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, described that it is used in serialization.

/// It describes stable serialization format, and only used by serialization logic.
#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
pub enum FunctionCallErrorSer {
/// Wasm compilation error
CompilationError(CompilationError),
/// Wasm binary env link error
Expand All @@ -34,13 +58,9 @@ pub enum FunctionCallError {
WasmUnknownError,
HostError(HostError),
EvmError(EvmError),
/// An error message when wasmer 1.0 returns a wasmer::RuntimeError
WasmerRuntimeError(String),
/// A trap in Wasmer 1.0, not same as WasmTrap above, String is a machine readable form like "stk_ovf"
/// String is used instead of wasmer internal enum is because of BorshSerializable.
/// It can be convert back by wasmer_vm::TrapCode::from_str
Wasmer1Trap(String),
ExecutionError(String),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd do

Suggested change
ExecutionError(String),
ExecutionError { msg: String },

just in case. This thing is serializable to JSON, and, tuple variants are not extensible:

#[derive(serde::Serialize, serde::Deserialize)]
enum E1 {
    V(String),
}

#[derive(serde::Serialize, serde::Deserialize)]
enum E2 {
    V { msg: String },
}

#[derive(serde::Serialize, serde::Deserialize)]
enum E3 {
    V { msg: String, code: u32 },
}


fn main() {
    println!("{}", serde_json::to_string(&E1::V("hello".into())).unwrap());
    println!("{}", serde_json::to_string(&E2::V { msg: "hello".into() }).unwrap());
    println!("{}", serde_json::to_string(&E3::V { msg: "hello".into(), code: 92 }).unwrap());
}

// OUTPUT
{"V":"hello"}
{"V":{"msg":"hello"}}
{"V":{"msg":"hello","code":92}}

Migrating E2 -> E3 is easier that E1 -> E3.

Now, because this is also borsh serializable, this might not be super-relevant, as adding a field to a borsh struct is not compatible, but it's better to be on the safe side here (and consistent with LinkError).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, done.

}

#[derive(
Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError,
)]
Expand All @@ -67,8 +87,8 @@ pub enum WasmTrap {
IllegalArithmetic,
/// Misaligned atomic access trap.
MisalignedAtomicAccess,
/// Breakpoint trap.
BreakpointTrap,
/// Indirect call to null.
IndirectCallToNull,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test case to trigger this error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, will do.

/// Stack overflow.
StackOverflow,
/// Generic trap.
Expand Down Expand Up @@ -363,12 +383,13 @@ impl fmt::Display for FunctionCallError {
FunctionCallError::HostError(e) => e.fmt(f),
FunctionCallError::LinkError { msg } => write!(f, "{}", msg),
FunctionCallError::WasmTrap(trap) => write!(f, "WebAssembly trap: {}", trap),
FunctionCallError::WasmUnknownError => {
write!(f, "Unknown error during Wasm contract execution")
FunctionCallError::WasmUnknownError { debug_message } => {
write!(f, "Unknown error during Wasm contract execution: {}", debug_message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still feel we should rename this to WasmInternalError, it's a paradox that it's unknown then error detail (debug_message) is given

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any renames like that would lead to serialization issues again. Think is general we shall go in direction of separatimg runtime and serialized data structures.

}
FunctionCallError::EvmError(e) => write!(f, "EVM: {:?}", e),
FunctionCallError::WasmerRuntimeError(e) => write!(f, "Wasmer Runtime: {}", e),
FunctionCallError::Wasmer1Trap(e) => write!(f, "Wasmer 1.0 trap: {}", e),
FunctionCallError::Nondeterministic(msg) => {
write!(f, "Nondeterministic error during contract execution: {}", msg)
}
}
}
}
Expand All @@ -387,8 +408,8 @@ impl fmt::Display for WasmTrap {
}
WasmTrap::MisalignedAtomicAccess => write!(f, "Misaligned atomic access trap."),
WasmTrap::GenericTrap => write!(f, "Generic trap."),
WasmTrap::BreakpointTrap => write!(f, "Breakpoint trap."),
WasmTrap::StackOverflow => write!(f, "Stack overflow."),
WasmTrap::IndirectCallToNull => write!(f, "Indirect call to null."),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions runtime/near-vm-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ wasmer-types = { version = "1.0.2", optional = true }
wasmer-compiler-singlepass = { version = "1.0.2", optional = true }
wasmer-compiler-cranelift = { version = "1.0.2", optional = true }
wasmer-engine-native = { version = "1.0.2", optional = true }
wasmer-vm = { version = "1.0.2" }
matklad marked this conversation as resolved.
Show resolved Hide resolved
pwasm-utils = "0.12"
parity-wasm = "0.41"
wasmtime = { version = "0.25.0", default-features = false, optional = true }
Expand Down
Loading