Skip to content

Commit

Permalink
Merge pull request #379 from stacks-network/feat/runtime-error-handling
Browse files Browse the repository at this point in the history
runtime error handling
  • Loading branch information
csgui authored Jun 27, 2024
2 parents 54964f1 + c9caf89 commit 47bf052
Show file tree
Hide file tree
Showing 28 changed files with 784 additions and 366 deletions.
161 changes: 161 additions & 0 deletions clar2wasm/src/error_mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use clarity::vm::errors::{CheckErrors, Error, RuntimeErrorType, ShortReturnType, WasmError};
use clarity::vm::types::ResponseData;
use clarity::vm::Value;
use wasmtime::{AsContextMut, Instance, Trap};

const LOG2_ERROR_MESSAGE: &str = "log2 must be passed a positive integer";
const SQRTI_ERROR_MESSAGE: &str = "sqrti must be passed a positive integer";
const POW_ERROR_MESSAGE: &str = "Power argument to (pow ...) must be a u32 integer";

pub enum ErrorMap {
NotClarityError = -1,
ArithmeticOverflow = 0,
ArithmeticUnderflow = 1,
DivisionByZero = 2,
ArithmeticLog2Error = 3,
ArithmeticSqrtiError = 4,
UnwrapFailure = 5,
Panic = 6,
ShortReturnAssertionFailure = 7,
ArithmeticPowError = 8,
NotMapped = 99,
}

impl From<i32> for ErrorMap {
fn from(error_code: i32) -> Self {
match error_code {
-1 => ErrorMap::NotClarityError,
0 => ErrorMap::ArithmeticOverflow,
1 => ErrorMap::ArithmeticUnderflow,
2 => ErrorMap::DivisionByZero,
3 => ErrorMap::ArithmeticLog2Error,
4 => ErrorMap::ArithmeticSqrtiError,
5 => ErrorMap::UnwrapFailure,
6 => ErrorMap::Panic,
7 => ErrorMap::ShortReturnAssertionFailure,
8 => ErrorMap::ArithmeticPowError,
_ => ErrorMap::NotMapped,
}
}
}

pub(crate) fn resolve_error(
e: wasmtime::Error,
instance: Instance,
mut store: impl AsContextMut,
) -> Error {
if let Some(vm_error) = e.root_cause().downcast_ref::<Error>() {
// SAFETY:
//
// This unsafe operation returns the value of a location pointed by `*mut T`.
//
// The purpose of this code is to take the ownership of the `vm_error` value
// since clarity::vm::errors::Error is not a Clonable type.
//
// Converting a `&T` (vm_error) to a `*mut T` doesn't cause any issues here
// because the reference is not borrowed elsewhere.
//
// The replaced `T` value is deallocated after the operation. Therefore, the chosen `T`
// is a dummy value, solely to satisfy the signature of the replace function
// and not cause harm when it is deallocated.
//
// Specifically, Error::Wasm(WasmError::ModuleNotFound) was selected as the placeholder value.
return unsafe {
core::ptr::replace(
(vm_error as *const Error) as *mut Error,
Error::Wasm(WasmError::ModuleNotFound),
)
};
}

if let Some(vm_error) = e.root_cause().downcast_ref::<CheckErrors>() {
// SAFETY:
//
// This unsafe operation returns the value of a location pointed by `*mut T`.
//
// The purpose of this code is to take the ownership of the `vm_error` value
// since clarity::vm::errors::Error is not a Clonable type.
//
// Converting a `&T` (vm_error) to a `*mut T` doesn't cause any issues here
// because the reference is not borrowed elsewhere.
//
// The replaced `T` value is deallocated after the operation. Therefore, the chosen `T`
// is a dummy value, solely to satisfy the signature of the replace function
// and not cause harm when it is deallocated.
//
// Specifically, CheckErrors::ExpectedName was selected as the placeholder value.
return unsafe {
let err = core::ptr::replace(
(vm_error as *const CheckErrors) as *mut CheckErrors,
CheckErrors::ExpectedName,
);

<CheckErrors as std::convert::Into<Error>>::into(err)
};
}

// Check if the error is caused by
// an unreachable Wasm trap.
//
// In this case, runtime errors are handled
// by being mapped to the corresponding ClarityWasm Errors.
if let Some(Trap::UnreachableCodeReached) = e.root_cause().downcast_ref::<Trap>() {
return from_runtime_error_code(instance, &mut store, e);
}

// All other errors are treated as general runtime errors.
Error::Wasm(WasmError::Runtime(e))
}

fn from_runtime_error_code(
instance: Instance,
mut store: impl AsContextMut,
e: wasmtime::Error,
) -> Error {
let global = "runtime-error-code";
let runtime_error_code = instance
.get_global(&mut store, global)
.and_then(|glob| glob.get(&mut store).i32())
.unwrap_or_else(|| panic!("Could not find {global} global with i32 value"));

match ErrorMap::from(runtime_error_code) {
ErrorMap::NotClarityError => Error::Wasm(WasmError::Runtime(e)),
ErrorMap::ArithmeticOverflow => {
Error::Runtime(RuntimeErrorType::ArithmeticOverflow, Some(Vec::new()))
}
ErrorMap::ArithmeticUnderflow => {
Error::Runtime(RuntimeErrorType::ArithmeticUnderflow, Some(Vec::new()))
}
ErrorMap::DivisionByZero => {
Error::Runtime(RuntimeErrorType::DivisionByZero, Some(Vec::new()))
}
ErrorMap::ArithmeticLog2Error => Error::Runtime(
RuntimeErrorType::Arithmetic(LOG2_ERROR_MESSAGE.into()),
Some(Vec::new()),
),
ErrorMap::ArithmeticSqrtiError => Error::Runtime(
RuntimeErrorType::Arithmetic(SQRTI_ERROR_MESSAGE.into()),
Some(Vec::new()),
),
ErrorMap::UnwrapFailure => {
Error::Runtime(RuntimeErrorType::UnwrapFailure, Some(Vec::new()))
}
ErrorMap::Panic => {
panic!("An error has been detected in the code")
}
// TODO: UInt(42) value below is just a placeholder.
// It should be replaced by the current "thrown-value" when issue #385 is resolved.
// Tests that reach this code are currently ignored.
ErrorMap::ShortReturnAssertionFailure => Error::ShortReturn(
ShortReturnType::AssertionFailed(Value::Response(ResponseData {
committed: false,
data: Box::new(Value::UInt(42)),
})),
),
ErrorMap::ArithmeticPowError => Error::Runtime(
RuntimeErrorType::Arithmetic(POW_ERROR_MESSAGE.into()),
Some(Vec::new()),
),
_ => panic!("Runtime error code {} not supported", runtime_error_code),
}
}
3 changes: 2 additions & 1 deletion clar2wasm/src/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use clarity::vm::{CallStack, ContractContext, Value};
use stacks_common::types::chainstate::StacksBlockId;
use wasmtime::{Engine, Linker, Module, Store};

use crate::error_mapping;
use crate::linker::link_host_functions;
use crate::wasm_utils::*;

Expand Down Expand Up @@ -368,7 +369,7 @@ pub fn initialize_contract(

top_level
.call(&mut store, &[], results.as_mut_slice())
.map_err(|e| Error::Wasm(WasmError::Runtime(e)))?;
.map_err(|e| error_mapping::resolve_error(e, instance, &mut store))?;

// Save the compiled Wasm module into the contract context
store.data_mut().contract_context_mut()?.set_wasm_module(
Expand Down
2 changes: 2 additions & 0 deletions clar2wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ mod words;
pub mod datastore;
pub mod tools;

mod error_mapping;

// FIXME: This is copied from stacks-blockchain
// Block limit in Stacks 2.1
pub const BLOCK_LIMIT_MAINNET_21: ExecutionCost = ExecutionCost {
Expand Down
41 changes: 31 additions & 10 deletions clar2wasm/src/standard/standard.wat
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,10 @@
;; Useful for debugging, just prints the value
(import "" "log" (func $log (param $value i64)))

;;
;; Global definitions
(global $stack-pointer (mut i32) (i32.const 0))
(export "stack-pointer" (global $stack-pointer))
(memory (export "memory") 10)
(global $runtime-error-code (mut i32) (i32.const -1))

;; (sha256) initial hash values: first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19
(data (i32.const 0) "\67\e6\09\6a\85\ae\67\bb\72\f3\6e\3c\3a\f5\4f\a5\7f\52\0e\51\8c\68\05\9b\ab\d9\83\1f\19\cd\e0\5b")
Expand Down Expand Up @@ -281,9 +282,10 @@
;; 5: buffer to integer expects a buffer length <= 16
;; 6: panic
;; 7: short return
(func $stdlib.runtime-error (param $error-code i32)
;; TODO: Implement runtime error
unreachable
(func $stdlib.runtime-error (param $error_code i32)
(global.set $runtime-error-code (local.get $error_code))

(unreachable)
)

;; This function can be used to add either signed or unsigned integers
Expand Down Expand Up @@ -1281,7 +1283,11 @@
(i64.gt_u (local.get $b_lo) (i64.const 127))
(i64.ne (local.get $b_hi) (i64.const 0))
)
(then (call $stdlib.runtime-error (i32.const 0)))
;; In the interpreter, overflows for the power operation throw a specific error,
;; not a general ArithmeticOverflow error.
;; Therefore, the runtime error code should be 8,
;; which maps to the specific error.
(then (call $stdlib.runtime-error (i32.const 8)))
)

;; shortcut if a == 2
Expand Down Expand Up @@ -1323,7 +1329,7 @@

;; otherwise, if b < 0 => runtime error
(if (i64.lt_s (local.get $b_hi) (i64.const 0))
(then (call $stdlib.runtime-error (i32.const 4)))
(then (call $stdlib.runtime-error (i32.const 8)))
)

;; if b > (a >= 0 ? 126 : 127) -> runtime error: overflow (since the biggest b that doesn't
Expand All @@ -1335,7 +1341,11 @@
)
(i64.ne (local.get $b_hi) (i64.const 0))
)
(then (call $stdlib.runtime-error (i32.const 0)))
;; In the interpreter, overflows for the power operation throw a specific error,
;; not a general ArithmeticOverflow error.
;; Therefore, the runtime error code should be 8,
;; which maps to the specific error.
(then (call $stdlib.runtime-error (i32.const 8)))
)

;; shortcut if a == 2
Expand Down Expand Up @@ -2767,7 +2777,7 @@
;;
(func $stdlib.to-uint (param $lo i64) (param $hi i64) (result i64 i64)
(if (i64.lt_s (local.get $hi) (i64.const 0))
(then (call $stdlib.runtime-error (i32.const 4)))
(then (call $stdlib.runtime-error (i32.const 1)))
)
(local.get $lo)
(local.get $hi)
Expand All @@ -2786,7 +2796,7 @@
;; Thus, if $hi >= 2^63 the argument is >= 2^127,
;; no matter what is present in $lo.
(if (i64.ge_u (local.get $hi) (i64.const 9223372036854775808))
(then (call $stdlib.runtime-error (i32.const 4)))
(then (call $stdlib.runtime-error (i32.const 0)))
)
(local.get $lo)
(local.get $hi)
Expand Down Expand Up @@ -3407,6 +3417,17 @@
(i32.const 1) (local.get $output-offset) (i32.sub (local.get $writeptr) (local.get $output-offset))
)

;;
;; Export section

;; Memory
(memory (export "memory") 10)

;; Globals
(export "stack-pointer" (global $stack-pointer))
(export "runtime-error-code" (global $runtime-error-code))

;; Functions
(export "stdlib.add-uint" (func $stdlib.add-uint))
(export "stdlib.add-int" (func $stdlib.add-int))
(export "stdlib.sub-uint" (func $stdlib.sub-uint))
Expand Down
Loading

0 comments on commit 47bf052

Please sign in to comment.