bobcat-panic 0.7.6

Bobcat utilities with a panic handler on Arbitrum Stylus.
Documentation
#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(feature = "alloc")]
extern crate alloc;

#[allow(unused)]
use core::fmt::{Result as FmtResult, Write};

use paste::paste;

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
mod impls {
    #[link(wasm_import_module = "console")]
    #[cfg(feature = "console")]
    unsafe extern "C" {
        pub(crate) fn log_txt(ptr: *const u8, len: usize);
    }

    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
    #[link(wasm_import_module = "vm_hooks")]
    #[allow(unused)]
    unsafe extern "C" {
        pub(crate) fn exit_early(code: i32) -> !;
        pub(crate) fn write_result(d: *const u8, l: usize);
        pub(crate) fn transient_store_bytes32(key: *const u8, value: *const u8);
        pub(crate) fn transient_load_bytes32(key: *const u8, value: *const u8);
    }
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
mod impls {
    pub unsafe fn transient_store_bytes32(_: *const u8, _: *const u8) {}
    pub unsafe fn transient_load_bytes32(_: *const u8, _: *const u8) {}
}

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
fn write_result_slice(s: &[u8]) {
    unsafe { impls::write_result(s.as_ptr(), s.len()) }
}

//Panic(uint256)
pub const PANIC_PREAMBLE_WORD: [u8; 32 + 4] = match const_hex::const_decode_to_array::<{ 32 + 4 }>(
    b"4e487b710000000000000000000000000000000000000000000000000000000000000000",
) {
    Ok(v) => v,
    Err(_) => panic!(),
};

//Error(string)
pub const ERROR_PREAMBLE_OFFSET: [u8; 4 + 32] = match const_hex::const_decode_to_array::<{ 4 + 32 }>(
    b"08c379a00000000000000000000000000000000000000000000000000000000000000020",
) {
    Ok(v) => v,
    Err(_) => panic!(),
};

#[derive(Clone, Debug, PartialEq)]
#[repr(u8)]
pub enum PanicCodes {
    DecodingError = 0,
    OverflowOrUnderflow = 0x11,
    DivByZero = 0x12,
}

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
pub fn panic_with_code(x: PanicCodes) -> ! {
    let mut b = PANIC_PREAMBLE_WORD;
    b[4 + 32 - 1] = x as u8;
    write_result_slice(&b);
    unsafe { impls::exit_early(1) }
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
pub fn panic_with_code(x: PanicCodes) -> ! {
    panic!("panicked with code: {x:?}");
}

#[macro_export]
macro_rules! define_panic_macros {
    (
        $(($error_msg:expr, $panic_code:ident)),* $(,)?
    ) => {
        $(
            paste! {
                #[macro_export]
                macro_rules! [<panic_on_err_ $error_msg>] {
                    ($e:expr, $msg:expr) => {{
                        match $e {
                            Some(v) => v,
                            None => {
                                #[cfg(feature = "msg-on-sdk-err")]
                                panic!("{}: {}", $error_msg, $msg);
                                #[cfg(not(feature = "msg-on-sdk-err"))]
                                $crate::panic_with_code($crate::PanicCodes::$panic_code);
                            }
                        }
                    }};
                }
            }
        )*
    };
}

define_panic_macros!(
    ("overflow", OverflowOrUnderflow),
    ("div_by_zero", DivByZero),
);

#[macro_export]
macro_rules! panic_on_err_bad_decoding_bool {
    ($msg:expr) => {{
        #[cfg(feature = "msg-on-sdk-err")]
        panic!("error decoding: {}", $msg);
        #[cfg(not(feature = "msg-on-sdk-err"))]
        $crate::panic_with_code($crate::PanicCodes::DecodingError);
    }};
    ($e:expr, $msg:expr) => {{
        if !$e {
            panic_on_err_bad_decoding_bool!($msg);
        }
    }};
}

#[allow(unused)]
struct SliceWriter<'a>(&'a mut [u8], usize);

impl<'a> Write for SliceWriter<'a> {
    fn write_str(&mut self, s: &str) -> FmtResult {
        let v = s.len().min(REVERT_BUF_SIZE.saturating_sub(self.1));
        self.0[self.1..self.1 + v].copy_from_slice(&s.as_bytes()[..v]);
        self.1 += v;
        Ok(())
    }
}

//uint256(keccak256(abi.encodePacked("bobcat.tracing.counter"))) - 1
pub const SLOT_TRACING_COUNTER: [u8; 32] = [
    0xad, 0x59, 0xcd, 0x5c, 0xcd, 0xcd, 0x00, 0x59, 0x2c, 0xd2, 0x06, 0xdc, 0x3b, 0xce, 0x83, 0xac,
    0xe6, 0x1b, 0x8c, 0x80, 0xcb, 0xe9, 0xfd, 0x0d, 0x70, 0x09, 0x34, 0xba, 0x13, 0x78, 0x92, 0x22,
];

/// Revert buffer size that's used to write the panic. We can afford to
/// use a large page here since a panic will consume all the gas anyway,
/// and a user will see this during simulation hopefully.
#[allow(unused)]
const REVERT_BUF_SIZE: usize = 1024 * 2;

#[cfg(all(feature = "panic-revert", feature = "panic-loc"))]
compile_error!("panic-revert and panic-loc simultaneously enabled");

#[cfg(all(feature = "panic-revert", feature = "panic-trace"))]
compile_error!("panic-revert and panic-trace simultaneously enabled");

#[cfg(all(feature = "panic-loc", feature = "panic-trace"))]
compile_error!("panic-loc and panic-trace simultaneously enabled");

#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
enum TracingDiscriminant {
    Number = 0,
    String = 1,
}

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
#[cfg_attr(all(feature = "panic", not(feature = "std")), panic_handler)]
pub fn panic_handler(_msg: &core::panic::PanicInfo) -> ! {
    #[cfg(feature = "console")]
    {
        let msg = alloc::format!("{_msg}");
        unsafe { impls::log_txt(msg.as_ptr(), msg.len()) }
    }
    #[cfg(any(
        feature = "panic-revert",
        feature = "panic-loc",
        feature = "panic-trace"
    ))]
    {
        let mut buf = [0u8; REVERT_BUF_SIZE];
        buf[..ERROR_PREAMBLE_OFFSET.len()].copy_from_slice(&ERROR_PREAMBLE_OFFSET);
        let mut w = SliceWriter(&mut buf[ERROR_PREAMBLE_OFFSET.len() + 32..], 0);
        #[cfg(feature = "panic-revert")]
        {
            write!(&mut w, "{_msg}").unwrap();
        }
        #[cfg(feature = "panic-loc")]
        if let Some(loc) = _msg.location() {
            write!(&mut w, "panic: {}:{}", loc.file(), loc.line()).unwrap();
        } else {
            write!(&mut w, "panic: unknown").unwrap();
        }
        #[cfg(feature = "panic-trace")]
        {
            let mut b = [0u8; 32];
            unsafe { impls::transient_load_bytes32(SLOT_TRACING_COUNTER.as_ptr(), b.as_mut_ptr()) };
            if b[0] == TracingDiscriminant::Number as u8 {
                write!(
                    &mut w,
                    "trace no: {}",
                    u32::from_be_bytes(b[1..32 - size_of::<u32>()].try_into().unwrap())
                )
            } else {
                write!(&mut w, "trace str: {}", trace_key_to_str(&b))
            }
            .unwrap()
        }
        let len_msg = w.1;
        let len_offset = ERROR_PREAMBLE_OFFSET.len();
        buf[len_offset + 28..len_offset + 32].copy_from_slice(&(len_msg as u32).to_be_bytes());
        let len_full = ERROR_PREAMBLE_OFFSET.len() + 32 + len_msg;
        let len_padded = len_full + (32 - (len_full % 32)) % 32;
        write_result_slice(&buf[..len_padded]);
        unsafe { impls::exit_early(1) }
    }
    // Prefer the normal behaviour if the user hasn't opted into this
    // feature. Maybe it's better to wipe out the revertdata if this happens,
    // the other behaviour is different.
    #[allow(unreachable_code)]
    core::arch::wasm32::unreachable()
}

pub fn bump() {
    let p = SLOT_TRACING_COUNTER.as_ptr();
    // We assume the execution counter here is always less than u32,
    // so the upper part of the word could be dirty!
    let mut b = [0u8; 32];
    unsafe { impls::transient_load_bytes32(p, b.as_mut_ptr()) };
    let v = u32::from_be_bytes(b[32 - size_of::<u32>()..].try_into().unwrap()) + 1;
    b[32 - size_of::<u32>()..].copy_from_slice(&v.to_be_bytes());
    b[0] = TracingDiscriminant::Number as u8;
    unsafe { impls::transient_store_bytes32(p, b.as_ptr()) }
}

pub const fn trace_key_of_str(s: &str) -> [u8; 32] {
    let bytes = s.as_bytes();
    let mut b = [0u8; 32];
    b[0] = TracingDiscriminant::String as u8;
    let mut i = 0;
    while i < bytes.len() && i < 31 {
        b[i + 1] = bytes[i];
        i += 1;
    }
    b
}

#[allow(unused)]
const fn trace_key_to_str(b: &[u8; 32]) -> &str {
    let mut i = 1;
    while i < 32 {
        if b[i] == 0 {
            break;
        }
        i += 1;
    }
    unsafe {
        let slice = core::slice::from_raw_parts(b.as_ptr().add(1), i - 1);
        core::str::from_utf8_unchecked(slice)
    }
}

pub fn trace(k: &str) {
    let v = trace_key_of_str(k);
    unsafe { impls::transient_store_bytes32(SLOT_TRACING_COUNTER.as_ptr(), v.as_ptr()) }
}

#[macro_export]
macro_rules! trace_guard {
    ($($body:tt)*) => {{
        trace(concat!(file!(), ":", line!()));
        $($body)*
    }};
}

#[cfg(all(test, feature = "std"))]
mod test {
    use proptest::prelude::*;

    use super::*;

    proptest! {
        #[test]
        fn test_key_back_and_forth(x in proptest::string::string_regex("[0-9a-zA-Z]{0,31}").unwrap()) {
            assert_eq!(&x, trace_key_to_str(&trace_key_of_str(&x)));
        }
    }
}