use std::cell::Cell;
use std::error::Error;
use std::fmt;
use std::panic;
use std::panic::UnwindSafe;
use std::sync::Once;
use thruput_trace::ffi_enums::InvokeErrorCode;
#[derive(Debug, Clone, PartialEq)]
pub struct InvokeError {
pub error_type: InvokeErrorCode,
}
impl fmt::Display for InvokeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "InvokeError {}", self.error_type)
}
}
impl Error for InvokeError {}
pub fn deterministic_error() -> InvokeError {
InvokeError {
error_type: InvokeErrorCode::Deterministic,
}
}
pub fn out_of_gas_error() -> InvokeError {
InvokeError {
error_type: InvokeErrorCode::OutOfGas,
}
}
pub fn unrecoverable_error() -> InvokeError {
InvokeError {
error_type: InvokeErrorCode::Unrecoverable,
}
}
pub fn control_flow_unwind() -> InvokeError {
InvokeError {
error_type: InvokeErrorCode::None,
}
}
pub fn validation_error() -> InvokeError {
InvokeError {
error_type: InvokeErrorCode::ValidationFailure,
}
}
pub fn abort_transaction_error() -> InvokeError {
InvokeError {
error_type: InvokeErrorCode::Abandon,
}
}
thread_local! {
static PANIC_HOOK_DEPTH: Cell<usize> = const { Cell::new(0) };
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PanicLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
impl std::fmt::Display for PanicLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PanicRecord {
pub message: Option<String>,
pub location: Option<PanicLocation>,
}
thread_local! {
static HOOKED_PANIC: std::cell::RefCell<Option<PanicRecord>> =
const { std::cell::RefCell::new(None) };
}
pub(crate) fn take_panic_record(
payload: &(dyn std::any::Any + Send),
) -> PanicRecord {
let message = panic_payload_message(payload);
let location = take_hooked_panic()
.filter(|hooked| hooked.message == message)
.and_then(|hooked| hooked.location);
PanicRecord { message, location }
}
fn take_hooked_panic() -> Option<PanicRecord> {
HOOKED_PANIC.with(|slot| slot.borrow_mut().take())
}
pub struct PanicHookGuard;
impl PanicHookGuard {
pub fn enter() -> Self {
install_panic_hook();
PANIC_HOOK_DEPTH.with(|c| c.set(c.get() + 1));
PanicHookGuard
}
}
impl Drop for PanicHookGuard {
fn drop(&mut self) {
PANIC_HOOK_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
}
}
static INSTALL_HOOK: Once = Once::new();
fn install_panic_hook() {
INSTALL_HOOK.call_once(|| {
let prev_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
if !is_panic_hook_active() {
prev_hook(panic_info);
return;
}
if panic_info.payload().downcast_ref::<InvokeError>().is_none() {
let record = PanicRecord {
message: panic_payload_message(panic_info.payload()),
location: panic_info.location().map(|l| PanicLocation {
file: l.file().to_string(),
line: l.line(),
column: l.column(),
}),
};
print_panic(&record, panic_info.payload());
HOOKED_PANIC.with(|slot| *slot.borrow_mut() = Some(record));
}
}));
});
}
fn print_panic(record: &PanicRecord, payload: &(dyn std::any::Any + Send)) {
match &record.message {
Some(message) => eprintln!("Panic message: {}", message),
None => eprintln!("Panic: {:?}", payload),
}
if let Some(location) = &record.location {
eprintln!(
"Panic occurred at file '{}' line {}",
location.file, location.line
);
}
}
pub(crate) fn is_panic_hook_active() -> bool {
PANIC_HOOK_DEPTH.with(|c| c.get()) > 0
}
fn panic_payload_message(
payload: &(dyn std::any::Any + Send),
) -> Option<String> {
if let Some(s) = payload.downcast_ref::<&'static str>() {
Some((*s).to_string())
} else {
payload.downcast_ref::<String>().cloned()
}
}
pub fn invoke_wrapper<'a, F>(
wrapped: F,
) -> Box<dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync + 'a>
where
F: Fn() + UnwindSafe + Clone + Send + Sync + 'a,
{
Box::new(move || {
let w = wrapped.clone();
let result = panic::catch_unwind(w);
match result {
Ok(_) => thruput_sys::ForeignInvokeResult {
panic: InvokeErrorCode::None.0,
},
Err(err) => {
if let Some(my_error) = err.downcast_ref::<InvokeError>() {
return thruput_sys::ForeignInvokeResult {
panic: my_error.error_type.0,
};
}
if is_panic_hook_active() {
drop(take_hooked_panic());
return thruput_sys::ForeignInvokeResult {
panic: InvokeErrorCode::Deterministic.0,
};
}
eprintln!("unrecoverable error: {:?}", err);
thruput_sys::ForeignInvokeResult {
panic: InvokeErrorCode::Unrecoverable.0,
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_body_panic_is_deterministic_on_any_thread() {
let unmarked = std::thread::spawn(|| {
invoke_wrapper(|| panic!("contract panic"))().panic
})
.join()
.expect("harness thread panicked");
assert_eq!(unmarked, InvokeErrorCode::Unrecoverable.0);
let in_body = std::thread::spawn(|| {
let _in_body = PanicHookGuard::enter();
invoke_wrapper(|| panic!("contract panic"))().panic
})
.join()
.expect("harness thread panicked");
assert_eq!(in_body, InvokeErrorCode::Deterministic.0);
}
#[test]
fn panic_hook_guard_balances_on_unwind() {
assert!(!is_panic_hook_active());
let caught = std::panic::catch_unwind(|| {
let _in_body = PanicHookGuard::enter();
assert!(is_panic_hook_active());
panic!("body panicked");
});
assert!(caught.is_err());
assert!(
!is_panic_hook_active(),
"guard must decrement when the body unwinds through it"
);
}
#[test]
fn display_terminates_and_names_the_error_code() {
let msg = deterministic_error().to_string();
assert!(msg.contains("InvokeError"), "got: {msg}");
assert!(msg.contains("Deterministic"), "got: {msg}");
}
#[test]
fn hook_never_panics_and_leaves_payload_untouched() {
let _in_body = PanicHookGuard::enter();
let err =
panic::catch_unwind(|| panic!("contract panic")).unwrap_err();
assert_eq!(
err.downcast_ref::<&str>(),
Some(&"contract panic"),
"the hook must let the original payload unwind unchanged; \
conversion is the catch site's job"
);
}
#[test]
fn invoke_wrapper_converts_panics_inside_a_body() {
{
let _in_body = PanicHookGuard::enter();
let result = invoke_wrapper(|| panic!("contract panic"))();
assert_eq!(result.panic, InvokeErrorCode::Deterministic.0);
let result =
invoke_wrapper(|| std::panic::panic_any(validation_error()))();
assert_eq!(result.panic, InvokeErrorCode::ValidationFailure.0);
}
assert!(!is_panic_hook_active());
let result = invoke_wrapper(|| panic!("outside a body"))();
assert_eq!(result.panic, InvokeErrorCode::Unrecoverable.0);
}
#[test]
fn nested_body_stays_active_after_inner_guard_drops() {
assert!(!is_panic_hook_active());
let outer = PanicHookGuard::enter();
{
let _inner = PanicHookGuard::enter();
}
assert!(is_panic_hook_active());
let result = invoke_wrapper(|| panic!("mid-nest panic"))();
assert_eq!(result.panic, InvokeErrorCode::Deterministic.0);
drop(outer);
assert!(!is_panic_hook_active());
}
}