bobcat_panic/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3#[cfg(feature = "alloc")]
4extern crate alloc;
5
6#[allow(unused)]
7use core::fmt::{Result as FmtResult, Write};
8
9use paste::paste;
10
11#[cfg(all(target_family = "wasm", target_os = "unknown"))]
12mod impls {
13    #[link(wasm_import_module = "console")]
14    #[cfg(feature = "console")]
15    unsafe extern "C" {
16        pub(crate) fn log_txt(ptr: *const u8, len: usize);
17    }
18
19    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
20    #[link(wasm_import_module = "vm_hooks")]
21    #[allow(unused)]
22    unsafe extern "C" {
23        pub(crate) fn exit_early(code: i32) -> !;
24        pub(crate) fn write_result(d: *const u8, l: usize);
25        pub(crate) fn transient_store_bytes32(key: *const u8, value: *const u8);
26        pub(crate) fn transient_load_bytes32(key: *const u8, value: *const u8);
27    }
28}
29
30#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
31mod impls {
32    pub unsafe fn transient_store_bytes32(_: *const u8, _: *const u8) {}
33    pub unsafe fn transient_load_bytes32(_: *const u8, _: *const u8) {}
34}
35
36#[cfg(all(target_family = "wasm", target_os = "unknown"))]
37fn write_result_slice(s: &[u8]) {
38    unsafe { impls::write_result(s.as_ptr(), s.len()) }
39}
40
41//Panic(uint256)
42pub const PANIC_PREAMBLE_WORD: [u8; 32 + 4] = match const_hex::const_decode_to_array::<{ 32 + 4 }>(
43    b"4e487b710000000000000000000000000000000000000000000000000000000000000000",
44) {
45    Ok(v) => v,
46    Err(_) => panic!(),
47};
48
49//Error(string)
50pub const ERROR_PREAMBLE_OFFSET: [u8; 4 + 32] = match const_hex::const_decode_to_array::<{ 4 + 32 }>(
51    b"08c379a00000000000000000000000000000000000000000000000000000000000000020",
52) {
53    Ok(v) => v,
54    Err(_) => panic!(),
55};
56
57#[derive(Clone, Debug, PartialEq)]
58#[repr(u8)]
59pub enum PanicCodes {
60    DecodingError = 0,
61    OverflowOrUnderflow = 0x11,
62    DivByZero = 0x12,
63}
64
65#[cfg(all(target_family = "wasm", target_os = "unknown"))]
66pub fn panic_with_code(x: PanicCodes) -> ! {
67    let mut b = PANIC_PREAMBLE_WORD;
68    b[4 + 32 - 1] = x as u8;
69    write_result_slice(&b);
70    unsafe { impls::exit_early(1) }
71}
72
73#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
74pub fn panic_with_code(x: PanicCodes) -> ! {
75    panic!("panicked with code: {x:?}");
76}
77
78#[macro_export]
79macro_rules! define_panic_macros {
80    (
81        $(($error_msg:expr, $panic_code:ident)),* $(,)?
82    ) => {
83        $(
84            paste! {
85                #[macro_export]
86                macro_rules! [<panic_on_err_ $error_msg>] {
87                    ($e:expr, $msg:expr) => {{
88                        match $e {
89                            Some(v) => v,
90                            None => {
91                                #[cfg(feature = "msg-on-sdk-err")]
92                                panic!("{}: {}", $error_msg, $msg);
93                                #[cfg(not(feature = "msg-on-sdk-err"))]
94                                $crate::panic_with_code($crate::PanicCodes::$panic_code);
95                            }
96                        }
97                    }};
98                }
99            }
100        )*
101    };
102}
103
104define_panic_macros!(
105    ("overflow", OverflowOrUnderflow),
106    ("div_by_zero", DivByZero),
107);
108
109#[macro_export]
110macro_rules! panic_on_err_bad_decoding_bool {
111    ($msg:expr) => {{
112        #[cfg(feature = "msg-on-sdk-err")]
113        panic!("error decoding: {}", $msg);
114        #[cfg(not(feature = "msg-on-sdk-err"))]
115        $crate::panic_with_code($crate::PanicCodes::DecodingError);
116    }};
117    ($e:expr, $msg:expr) => {{
118        if !$e {
119            panic_on_err_bad_decoding_bool!($msg);
120        }
121    }};
122}
123
124#[allow(unused)]
125struct SliceWriter<'a>(&'a mut [u8], usize);
126
127impl<'a> Write for SliceWriter<'a> {
128    fn write_str(&mut self, s: &str) -> FmtResult {
129        let v = s.len().min(REVERT_BUF_SIZE.saturating_sub(self.1));
130        self.0[self.1..self.1 + v].copy_from_slice(&s.as_bytes()[..v]);
131        self.1 += v;
132        Ok(())
133    }
134}
135
136//uint256(keccak256(abi.encodePacked("bobcat.tracing.counter"))) - 1
137pub const SLOT_TRACING_COUNTER: [u8; 32] = [
138    0xad, 0x59, 0xcd, 0x5c, 0xcd, 0xcd, 0x00, 0x59, 0x2c, 0xd2, 0x06, 0xdc, 0x3b, 0xce, 0x83, 0xac,
139    0xe6, 0x1b, 0x8c, 0x80, 0xcb, 0xe9, 0xfd, 0x0d, 0x70, 0x09, 0x34, 0xba, 0x13, 0x78, 0x92, 0x22,
140];
141
142/// Revert buffer size that's used to write the panic. We can afford to
143/// use a large page here since a panic will consume all the gas anyway,
144/// and a user will see this during simulation hopefully.
145#[allow(unused)]
146const REVERT_BUF_SIZE: usize = 1024 * 2;
147
148#[cfg(all(feature = "panic-revert", feature = "panic-loc"))]
149compile_error!("panic-revert and panic-loc simultaneously enabled");
150
151#[cfg(all(feature = "panic-revert", feature = "panic-trace"))]
152compile_error!("panic-revert and panic-trace simultaneously enabled");
153
154#[cfg(all(feature = "panic-loc", feature = "panic-trace"))]
155compile_error!("panic-loc and panic-trace simultaneously enabled");
156
157#[derive(Debug, Clone, Copy, PartialEq)]
158#[repr(u8)]
159enum TracingDiscriminant {
160    Number = 0,
161    String = 1,
162}
163
164#[cfg(all(target_family = "wasm", target_os = "unknown"))]
165#[cfg_attr(all(feature = "panic", not(feature = "std")), panic_handler)]
166pub fn panic_handler(_msg: &core::panic::PanicInfo) -> ! {
167    #[cfg(feature = "console")]
168    {
169        let msg = alloc::format!("{_msg}");
170        unsafe { impls::log_txt(msg.as_ptr(), msg.len()) }
171    }
172    #[cfg(any(
173        feature = "panic-revert",
174        feature = "panic-loc",
175        feature = "panic-trace"
176    ))]
177    {
178        let mut buf = [0u8; REVERT_BUF_SIZE];
179        buf[..ERROR_PREAMBLE_OFFSET.len()].copy_from_slice(&ERROR_PREAMBLE_OFFSET);
180        let mut w = SliceWriter(&mut buf[ERROR_PREAMBLE_OFFSET.len() + 32..], 0);
181        #[cfg(feature = "panic-revert")]
182        {
183            write!(&mut w, "{_msg}").unwrap();
184        }
185        #[cfg(feature = "panic-loc")]
186        if let Some(loc) = _msg.location() {
187            write!(&mut w, "panic: {}:{}", loc.file(), loc.line()).unwrap();
188        } else {
189            write!(&mut w, "panic: unknown").unwrap();
190        }
191        #[cfg(feature = "panic-trace")]
192        {
193            let mut b = [0u8; 32];
194            unsafe { impls::transient_load_bytes32(SLOT_TRACING_COUNTER.as_ptr(), b.as_mut_ptr()) };
195            if b[0] == TracingDiscriminant::Number as u8 {
196                write!(
197                    &mut w,
198                    "trace no: {}",
199                    u32::from_be_bytes(b[1..32 - size_of::<u32>()].try_into().unwrap())
200                )
201            } else {
202                write!(&mut w, "trace str: {}", trace_key_to_str(&b))
203            }
204            .unwrap()
205        }
206        let len_msg = w.1;
207        let len_offset = ERROR_PREAMBLE_OFFSET.len();
208        buf[len_offset + 28..len_offset + 32].copy_from_slice(&(len_msg as u32).to_be_bytes());
209        let len_full = ERROR_PREAMBLE_OFFSET.len() + 32 + len_msg;
210        let len_padded = len_full + (32 - (len_full % 32)) % 32;
211        write_result_slice(&buf[..len_padded]);
212        unsafe { impls::exit_early(1) }
213    }
214    // Prefer the normal behaviour if the user hasn't opted into this
215    // feature. Maybe it's better to wipe out the revertdata if this happens,
216    // the other behaviour is different.
217    #[allow(unreachable_code)]
218    core::arch::wasm32::unreachable()
219}
220
221#[cfg(all(target_arch = "riscv32", target_os = "unknown"))]
222#[cfg_attr(all(feature = "panic", not(feature = "std")), panic_handler)]
223pub fn panic_handler(_: &core::panic::PanicInfo) -> ! {
224    // TODO: this needs to be fleshed out
225    unsafe {
226        core::arch::asm!("ebreak");
227    }
228    loop {}
229}
230
231pub fn bump() {
232    let p = SLOT_TRACING_COUNTER.as_ptr();
233    // We assume the execution counter here is always less than u32,
234    // so the upper part of the word could be dirty!
235    let mut b = [0u8; 32];
236    unsafe { impls::transient_load_bytes32(p, b.as_mut_ptr()) };
237    let v = u32::from_be_bytes(b[32 - size_of::<u32>()..].try_into().unwrap()) + 1;
238    b[32 - size_of::<u32>()..].copy_from_slice(&v.to_be_bytes());
239    b[0] = TracingDiscriminant::Number as u8;
240    unsafe { impls::transient_store_bytes32(p, b.as_ptr()) }
241}
242
243pub const fn trace_key_of_str(s: &str) -> [u8; 32] {
244    let bytes = s.as_bytes();
245    let mut b = [0u8; 32];
246    b[0] = TracingDiscriminant::String as u8;
247    let mut i = 0;
248    while i < bytes.len() && i < 31 {
249        b[i + 1] = bytes[i];
250        i += 1;
251    }
252    b
253}
254
255#[allow(unused)]
256const fn trace_key_to_str(b: &[u8; 32]) -> &str {
257    let mut i = 1;
258    while i < 32 {
259        if b[i] == 0 {
260            break;
261        }
262        i += 1;
263    }
264    unsafe {
265        let slice = core::slice::from_raw_parts(b.as_ptr().add(1), i - 1);
266        core::str::from_utf8_unchecked(slice)
267    }
268}
269
270pub fn trace(k: &str) {
271    let v = trace_key_of_str(k);
272    unsafe { impls::transient_store_bytes32(SLOT_TRACING_COUNTER.as_ptr(), v.as_ptr()) }
273}
274
275#[macro_export]
276macro_rules! trace_guard {
277    ($($body:tt)*) => {{
278        trace(concat!(file!(), ":", line!()));
279        $($body)*
280    }};
281}
282
283#[cfg(all(test, feature = "std"))]
284mod test {
285    use proptest::prelude::*;
286
287    use super::*;
288
289    proptest! {
290        #[test]
291        fn test_key_back_and_forth(x in proptest::string::string_regex("[0-9a-zA-Z]{0,31}").unwrap()) {
292            assert_eq!(&x, trace_key_to_str(&trace_key_of_str(&x)));
293        }
294    }
295}