Skip to main content

forest/rpc/methods/eth/
utils.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::types::{EthAddress, EthBytes};
5use crate::prelude::*;
6use crate::rpc::state::{MessageTrace, ReturnTrace};
7use crate::shim::actors::{EVMActorStateLoad as _, evm, is_evm_actor};
8use crate::shim::address::Address as FilecoinAddress;
9use crate::shim::fvm_shared_latest::IDENTITY_HASH;
10use crate::shim::state_tree::{ActorState, StateTree};
11use crate::utils::encoding::hex;
12use ahash::HashMap;
13
14use crate::rpc::eth::{EVM_WORD_LENGTH, EthUint64};
15use crate::shim::actors::evm::U256;
16use anyhow::{Result, bail};
17use cbor4ii::core::Value;
18use cbor4ii::core::dec::Decode as _;
19use fvm_ipld_encoding::{CBOR, DAG_CBOR, IPLD_RAW, RawBytes};
20use fvm_ipld_kamt::{AsHashedKey, Config as KamtConfig, HashedKey, Kamt};
21use serde::de;
22use std::borrow::Cow;
23use std::sync::LazyLock;
24
25/// KAMT configuration matching the EVM actor in builtin-actors.
26// <https://github.com/filecoin-project/builtin-actors/blob/v18.0.0/actors/evm/src/interpreter/system.rs#L47>
27pub(crate) fn evm_kamt_config() -> KamtConfig {
28    KamtConfig {
29        bit_width: 5,
30        min_data_depth: 0,
31        max_array_width: 1,
32    }
33}
34
35/// Hash algorithm for the EVM storage KAMT: the key's big-endian bytes are the hash.
36// <https://github.com/filecoin-project/builtin-actors/blob/v18.0.0/actors/evm/src/interpreter/system.rs#L49>
37pub(crate) struct EvmStateHashAlgorithm;
38
39impl AsHashedKey<U256, 32> for EvmStateHashAlgorithm {
40    fn as_hashed_key(key: &U256) -> Cow<'_, HashedKey<32>> {
41        Cow::Owned(key.to_big_endian())
42    }
43}
44
45pub(crate) type EvmStorageKamt<BS> = Kamt<BS, U256, U256, EvmStateHashAlgorithm>;
46
47pub fn lookup_eth_address<DB: Blockstore>(
48    addr: &FilecoinAddress,
49    state: &StateTree<DB>,
50) -> Result<Option<EthAddress>> {
51    // Attempt to convert directly, if it's an f4 address.
52    if let Ok(eth_addr) = EthAddress::from_filecoin_address(addr)
53        && !eth_addr.is_masked_id()
54    {
55        return Ok(Some(eth_addr));
56    }
57
58    // Otherwise, resolve the ID addr.
59    let id_addr = match state.lookup_id(addr)? {
60        Some(id) => id,
61        _ => return Ok(None),
62    };
63
64    // Lookup on the target actor and try to get an f410 address.
65    let result = state.get_actor(addr);
66    if let Ok(Some(actor_state)) = result {
67        if let Some(addr) = actor_state.delegated_address {
68            if let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr.into())
69                && !eth_addr.is_masked_id()
70            {
71                // Conversable into an eth address, use it.
72                return Ok(Some(eth_addr));
73            }
74        } else {
75            // No delegated address -> use a masked ID address
76        }
77    } else if let Ok(None) = result {
78        // Not found -> use a masked ID address
79    } else {
80        // Any other error -> fail.
81        result?;
82    }
83
84    // Otherwise, use the masked address.
85    Ok(Some(EthAddress::from_actor_id(id_addr)))
86}
87
88/// The actor's EVM state, or `None` when it is not an EVM actor.
89fn evm_state<DB: Blockstore>(store: &DB, actor: &ActorState) -> anyhow::Result<Option<evm::State>> {
90    if !is_evm_actor(&actor.code) {
91        return Ok(None);
92    }
93    Ok(Some(
94        evm::State::load(store, actor.code, actor.state).context("failed to load EVM state")?,
95    ))
96}
97
98/// As [`evm_state`], but also `None` for a dead (self-destructed) contract, which reads as empty.
99// <https://github.com/filecoin-project/builtin-actors/blob/v18.0.0/actors/evm/src/interpreter/system.rs#L181>
100pub(crate) fn live_evm_state<DB: Blockstore>(
101    store: &DB,
102    actor: &ActorState,
103) -> anyhow::Result<Option<evm::State>> {
104    Ok(evm_state(store, actor)?.filter(|state| state.is_alive()))
105}
106
107/// Extension trait for querying Ethereum-relevant state from a Filecoin actor.
108pub(crate) trait ActorStateEthExt {
109    /// Returns the effective nonce: EVM nonce for EVM actors (zero once self-destructed),
110    /// sequence otherwise.
111    fn eth_nonce<DB: Blockstore>(&self, store: &DB) -> anyhow::Result<EthUint64>;
112    /// Returns the deployed bytecode of an EVM actor, or `None` for non-EVM or self-destructed actors.
113    fn eth_bytecode<DB: Blockstore>(&self, store: &DB) -> anyhow::Result<Option<EthBytes>>;
114    /// Returns the 32-byte storage value at `position`, matching the EVM actor's `GetStorageAt`: a
115    /// zeroed word for a non-EVM actor, a dead (tombstoned) contract, or an unset slot.
116    fn eth_storage_at<DB: Blockstore>(
117        &self,
118        store: &DB,
119        position: &[u8; EVM_WORD_LENGTH],
120    ) -> anyhow::Result<[u8; EVM_WORD_LENGTH]>;
121}
122
123impl ActorStateEthExt for ActorState {
124    fn eth_nonce<DB: Blockstore>(&self, store: &DB) -> anyhow::Result<EthUint64> {
125        Ok(EthUint64(match evm_state(store, self)? {
126            Some(state) if state.is_alive() => state.nonce(),
127            // A dead contract's state still carries a nonce, but it reports zero.
128            Some(_) => 0,
129            None => self.sequence,
130        }))
131    }
132
133    fn eth_bytecode<DB: Blockstore>(&self, store: &DB) -> anyhow::Result<Option<EthBytes>> {
134        let Some(evm_state) = live_evm_state(store, self)? else {
135            return Ok(None);
136        };
137        let bytecode = store
138            .get(&evm_state.bytecode())
139            .context("failed to read EVM bytecode")?;
140        Ok(bytecode.map(EthBytes))
141    }
142
143    fn eth_storage_at<DB: Blockstore>(
144        &self,
145        store: &DB,
146        position: &[u8; EVM_WORD_LENGTH],
147    ) -> anyhow::Result<[u8; EVM_WORD_LENGTH]> {
148        // Mirrors the EVM actor's `GetStorageAt`.
149        // <https://github.com/filecoin-project/builtin-actors/blob/v18.0.0/actors/evm/src/lib.rs#L309>
150        let Some(evm_state) = live_evm_state(store, self)? else {
151            return Ok([0; EVM_WORD_LENGTH]);
152        };
153        let kamt =
154            EvmStorageKamt::load_with_config(&evm_state.contract_state(), store, evm_kamt_config())
155                .context("failed to load EVM storage KAMT")?;
156        let value = kamt
157            .get(&U256::from_big_endian(position))
158            .context("failed to read EVM storage slot")?
159            .copied()
160            .unwrap_or_default();
161        Ok(value.to_big_endian())
162    }
163}
164
165/// Decodes the payload using the given codec.
166pub fn decode_payload(payload: &RawBytes, codec: u64) -> Result<EthBytes> {
167    match codec {
168        IDENTITY_HASH => Ok(EthBytes::default()),
169        DAG_CBOR | CBOR => {
170            let mut reader = cbor4ii::core::utils::SliceReader::new(payload.bytes());
171            match Value::decode(&mut reader) {
172                Ok(Value::Bytes(bytes)) => Ok(EthBytes(bytes)),
173                other => {
174                    tracing::debug!(
175                        "failed to decode params byte array: {other:?}, codec: {codec}, payload: {}",
176                        hex::encode(payload.bytes())
177                    );
178                    bail!("failed to decode params byte array");
179                }
180            }
181        }
182        IPLD_RAW => Ok(EthBytes(payload.to_vec())),
183        _ => bail!("decode_payload: unsupported codec {codec}"),
184    }
185}
186
187/// Decodes the message trace params using the message trace codec.
188pub fn decode_params<'a, T>(trace: &'a MessageTrace) -> anyhow::Result<T>
189where
190    T: de::Deserialize<'a>,
191{
192    let codec = trace.params_codec;
193    match codec {
194        DAG_CBOR | CBOR => fvm_ipld_encoding::from_slice(&trace.params)
195            .map_err(|e| anyhow::anyhow!("failed to decode params: {e}")),
196        _ => bail!("Method called an unexpected codec {codec}"),
197    }
198}
199
200/// Decodes the return bytes using the return trace codec.
201pub fn decode_return<'a, T>(trace: &'a ReturnTrace) -> anyhow::Result<T>
202where
203    T: de::Deserialize<'a>,
204{
205    let codec = trace.return_codec;
206    match codec {
207        DAG_CBOR | CBOR => fvm_ipld_encoding::from_slice(trace.r#return.bytes())
208            .map_err(|e| anyhow::anyhow!("failed to decode return value: {e}")),
209        _ => bail!("Method returned an unexpected codec {codec}"),
210    }
211}
212
213/// Extract and decode Ethereum revert reason from receipt return data
214pub fn decode_revert_reason(return_data: RawBytes) -> (Vec<u8>, String) {
215    match decode_payload(&return_data, CBOR) {
216        Err(_) => (Vec::new(), String::new()),
217        Ok(data) if !data.is_empty() => {
218            let reason = parse_eth_revert(data.as_slice());
219            (data.0, reason)
220        }
221        Ok(data) => (data.0, "none".to_string()),
222    }
223}
224
225const ERROR_FUNCTION_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0]; // keccak256("Error(string)") [first 4 bytes]
226const PANIC_FUNCTION_SELECTOR: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71]; // keccak256("Panic(uint256)") [first 4 bytes]
227
228// Lazily initialized HashMap for panic codes
229static PANIC_ERROR_CODES: LazyLock<HashMap<u64, &'static str>> = LazyLock::new(|| {
230    let mut m = HashMap::new();
231    m.insert(0x00, "Panic()");
232    m.insert(0x01, "Assert()");
233    m.insert(0x11, "ArithmeticOverflow()");
234    m.insert(0x12, "DivideByZero()");
235    m.insert(0x21, "InvalidEnumVariant()");
236    m.insert(0x22, "InvalidStorageArray()");
237    m.insert(0x31, "PopEmptyArray()");
238    m.insert(0x32, "ArrayIndexOutOfBounds()");
239    m.insert(0x41, "OutOfMemory()");
240    m.insert(0x51, "CalledUninitializedFunction()");
241    m
242});
243
244/// EVM error and panic related constants
245const EVM_FUNC_SELECTOR_LENGTH: usize = 4;
246const EVM_PANIC_CODE_LENGTH: usize = 32;
247const EVM_UINT_PADDING_LENGTH: usize = 24;
248
249/// Parse an ABI encoded revert reason from a raw return value.
250///
251/// Handles both `Error(string)` and `Panic(uint256)` formats according to
252/// Solidity's revert conventions.
253///
254/// See https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
255pub(crate) fn parse_eth_revert(data: &[u8]) -> String {
256    // If it's not long enough to contain an ABI encoded response, return immediately.
257    if data.len() < EVM_FUNC_SELECTOR_LENGTH + EVM_WORD_LENGTH {
258        return hex::encode_prefixed(data);
259    }
260
261    // Extract function selector (first 4 bytes)
262    let selector = data
263        .get(..EVM_FUNC_SELECTOR_LENGTH)
264        .expect("checked data length >= 4");
265
266    match selector {
267        selector if selector == PANIC_FUNCTION_SELECTOR.as_slice() => parse_panic_revert(data),
268        selector if selector == ERROR_FUNCTION_SELECTOR.as_slice() => parse_error_revert(data),
269        _ => hex::encode_prefixed(data),
270    }
271}
272
273fn parse_error_revert(data: &[u8]) -> String {
274    let fallback = || hex::encode_prefixed(data);
275
276    let parse_result: Result<String, ()> = (|| {
277        let data = data
278            .get(EVM_FUNC_SELECTOR_LENGTH..)
279            .filter(|d| d.len() >= EVM_WORD_LENGTH)
280            .ok_or(())?;
281
282        // Get offset, from the first 32 bytes of the data
283        let offset_bytes = data.get(..EVM_WORD_LENGTH).ok_or(())?;
284        let offset = EthUint64::from_bytes(offset_bytes).map_err(|_| ())?.0 as usize;
285
286        // Validate offset range
287        if offset >= data.len() || data.len().saturating_sub(offset) < EVM_WORD_LENGTH {
288            return Err(());
289        }
290
291        // Get string length, from the offset + 32 bytes of the data
292        let length_bytes = data.get(offset..offset + EVM_WORD_LENGTH).ok_or(())?;
293        let len = EthUint64::from_bytes(length_bytes).map_err(|_| ())?.0 as usize;
294
295        // Validate string length
296        let string_start = offset + EVM_WORD_LENGTH;
297        if string_start > data.len() || len > data.len() - string_start {
298            return Err(());
299        }
300
301        // Attempt to decode valid UTF-8
302        let string = data.get(string_start..string_start + len).ok_or(())?;
303        Ok(format!(
304            "Error({})",
305            std::str::from_utf8(string).map_err(|_| ())?
306        ))
307    })();
308
309    parse_result.unwrap_or_else(|_| fallback())
310}
311
312fn parse_panic_revert(data: &[u8]) -> String {
313    let fallback = || hex::encode_prefixed(data);
314
315    let parse_result: Result<String, ()> = (|| {
316        let code_bytes = data
317            .get(EVM_FUNC_SELECTOR_LENGTH..EVM_FUNC_SELECTOR_LENGTH + EVM_PANIC_CODE_LENGTH)
318            .ok_or(())?;
319
320        // Check if first 24 bytes are all zeros
321        if !code_bytes
322            .get(..EVM_UINT_PADDING_LENGTH)
323            .ok_or(())?
324            .iter()
325            .all(|&v| v == 0)
326        {
327            return Ok(format!("Panic(0x{})", hex::encode(code_bytes)));
328        }
329
330        let code_data = code_bytes.get(..EVM_WORD_LENGTH).ok_or(())?;
331        let code = EthUint64::from_bytes(code_data).map_err(|_| ())?.0;
332        Ok(PANIC_ERROR_CODES
333            .get(&code)
334            .map(|s| s.to_string())
335            .unwrap_or_else(|| format!("Panic(0x{code:x})")))
336    })();
337
338    parse_result.unwrap_or_else(|_| fallback())
339}
340
341#[cfg(test)]
342mod test {
343    use super::*;
344    use rstest::rstest;
345
346    #[test]
347    fn eth_storage_at_matches_evm_storage() {
348        use crate::db::MemoryDB;
349        use crate::networks::ACTOR_BUNDLES_METADATA;
350        use crate::shim::econ::TokenAmount;
351        use crate::shim::machine::BuiltinActor;
352        use crate::utils::db::CborStoreExt as _;
353
354        fn word(n: u8) -> [u8; EVM_WORD_LENGTH] {
355            let mut w = [0; EVM_WORD_LENGTH];
356            w[EVM_WORD_LENGTH - 1] = n;
357            w
358        }
359        let store = MemoryDB::default();
360        let zero = word(0);
361        // Newest bundled EVM actor CID, matching the version `default_latest_version` builds.
362        let evm_code_cid = ACTOR_BUNDLES_METADATA
363            .values()
364            .filter_map(|bundle| {
365                Some((
366                    bundle.actor_major_version().ok()?,
367                    bundle.manifest.get(BuiltinActor::EVM).ok()?,
368                ))
369            })
370            .max_by_key(|&(version, _)| version)
371            .expect("bundled EVM actor code CID")
372            .1;
373        let evm_actor = |state: &evm::State| {
374            let state_cid = store.put_cbor_default(state).unwrap();
375            ActorState::new(evm_code_cid, state_cid, TokenAmount::default(), 0, None)
376        };
377
378        // A non-EVM actor has no EVM storage: reads as a zero word.
379        let non_evm = ActorState::new(
380            Cid::default(),
381            Cid::default(),
382            TokenAmount::default(),
383            0,
384            None,
385        );
386        assert_eq!(non_evm.eth_storage_at(&store, &zero).unwrap(), zero);
387
388        // A live contract returns the stored slot value, and a zero word for an unset slot.
389        let mut slots = EvmStorageKamt::new_with_config(&store, evm_kamt_config());
390        slots.set(U256::from(5u64), U256::from(42u64)).unwrap();
391        let contract_state = slots.flush().unwrap();
392        let with_tombstone = |tombstone| {
393            evm_actor(&evm::State::default_latest_version(
394                Cid::default(),
395                [0; 32],
396                contract_state,
397                None,
398                0,
399                tombstone,
400            ))
401        };
402        let alive = with_tombstone(None);
403        assert_eq!(alive.eth_storage_at(&store, &word(5)).unwrap(), word(42));
404        assert_eq!(alive.eth_storage_at(&store, &word(6)).unwrap(), zero);
405
406        // A dead (tombstoned) contract reads a zero word even though the same slots are populated.
407        let dead = with_tombstone(Some(evm::Tombstone {
408            origin: 100,
409            nonce: 1,
410        }));
411        assert_eq!(dead.eth_storage_at(&store, &word(5)).unwrap(), zero);
412    }
413
414    fn create_error_data(msg: &str) -> Vec<u8> {
415        let mut encoded = Vec::new();
416
417        // Step 1: Add function selector (keccak256("Error(string)") first 4 bytes)
418        encoded.extend_from_slice(&[0x08, 0xc3, 0x79, 0xa0]);
419
420        // Add offset to string data (32 bytes, value = 32)
421        // This points to where the string length is stored
422        let mut offset_bytes = [0u8; 32];
423        offset_bytes[24..32].copy_from_slice(&32u64.to_be_bytes());
424        encoded.extend_from_slice(&offset_bytes);
425
426        // Add string length (32 bytes)
427        let mut length_bytes = [0u8; 32];
428        length_bytes[24..32].copy_from_slice(&(msg.len() as u64).to_be_bytes());
429        encoded.extend_from_slice(&length_bytes);
430
431        // Add string data
432        encoded.extend_from_slice(msg.as_bytes());
433
434        // Pad to 32-byte boundary
435        let padding_needed = (32 - (msg.len() % 32)) % 32;
436        encoded.extend_from_slice(&vec![0; padding_needed]);
437
438        encoded
439    }
440
441    fn create_panic_data(code: u64) -> Vec<u8> {
442        let mut data = Vec::new();
443        data.extend_from_slice(&PANIC_FUNCTION_SELECTOR);
444
445        // Add padding (24 bytes) + code (32 bytes)
446        data.extend_from_slice(&[0; 24]);
447        data.extend_from_slice(&code.to_be_bytes());
448        data
449    }
450
451    #[test]
452    fn test_all_valid_parse_panic_revert() {
453        for (code, msg) in PANIC_ERROR_CODES.iter() {
454            let data = create_panic_data(*code);
455            assert_eq!(parse_panic_revert(&data), msg.to_string());
456        }
457    }
458
459    #[test]
460    fn test_all_valid_hex_parse_error_revert() {
461        let panic_data =
462            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000000")
463                .unwrap();
464        assert_eq!(parse_panic_revert(&panic_data), "Panic()");
465
466        let assert_data =
467            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000001")
468                .unwrap();
469        assert_eq!(parse_panic_revert(&assert_data), "Assert()");
470
471        let arithmetic_overflow_data =
472            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000011")
473                .unwrap();
474        assert_eq!(
475            parse_panic_revert(&arithmetic_overflow_data),
476            "ArithmeticOverflow()"
477        );
478
479        let divide_by_zero_data =
480            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000012")
481                .unwrap();
482        assert_eq!(parse_panic_revert(&divide_by_zero_data), "DivideByZero()");
483
484        let invalid_enum_variant_data =
485            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000021")
486                .unwrap();
487        assert_eq!(
488            parse_panic_revert(&invalid_enum_variant_data),
489            "InvalidEnumVariant()"
490        );
491
492        let invalid_storage_array_data =
493            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000022")
494                .unwrap();
495        assert_eq!(
496            parse_panic_revert(&invalid_storage_array_data),
497            "InvalidStorageArray()"
498        );
499
500        let pop_empty_array_data =
501            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000031")
502                .unwrap();
503        assert_eq!(parse_panic_revert(&pop_empty_array_data), "PopEmptyArray()");
504
505        let array_index_out_of_bounds_data =
506            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000032")
507                .unwrap();
508        assert_eq!(
509            parse_panic_revert(&array_index_out_of_bounds_data),
510            "ArrayIndexOutOfBounds()"
511        );
512
513        let out_of_memory_data =
514            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000041")
515                .unwrap();
516        assert_eq!(parse_panic_revert(&out_of_memory_data), "OutOfMemory()");
517
518        let call_uninitialized_data =
519            hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000051")
520                .unwrap();
521        assert_eq!(
522            parse_panic_revert(&call_uninitialized_data),
523            "CalledUninitializedFunction()"
524        );
525    }
526
527    #[test]
528    fn test_parse_error_revert() {
529        let err_msg = "Not enough Ether provided";
530        let error_data = create_error_data(err_msg);
531        assert_eq!(parse_error_revert(&error_data), format!("Error({err_msg})"));
532
533        // ABI-encoded Error("Hello World")
534        let err_data = hex::decode(
535            "\
536            08c379a0\
537            0000000000000000000000000000000000000000000000000000000000000020\
538            000000000000000000000000000000000000000000000000000000000000000b\
539            48656c6c6f20576f726c64000000000000000000000000000000000000000000\
540            ",
541        )
542        .unwrap();
543        assert_eq!(parse_error_revert(&err_data), "Error(Hello World)");
544
545        // ERC20 insufficient balance
546        let insufficient = hex::decode(
547            "08c379a0\
548                0000000000000000000000000000000000000000000000000000000000000020\
549                0000000000000000000000000000000000000000000000000000000000000026\
550                45524332303a207472616e7366657220616d6f756e7420657863656564732062\
551                616c616e63650000000000000000000000000000000000000000000000000000",
552        )
553        .unwrap();
554        assert_eq!(
555            parse_eth_revert(&insufficient),
556            "Error(ERC20: transfer amount exceeds balance)"
557        );
558    }
559
560    #[test]
561    fn test_parse_eth_revert_main_function() {
562        // Test normal Error case
563        let message = "Transaction failed";
564        let data = create_error_data(message);
565        assert_eq!(parse_eth_revert(&data), format!("Error({message})"));
566
567        // Test normal Panic case
568        let panic_data = create_panic_data(0x01); // Assert()
569        assert_eq!(parse_eth_revert(&panic_data), "Assert()");
570
571        // Test data too short for any revert reason
572        let short_data = vec![0x1, 0x2, 0x3];
573        assert_eq!(
574            parse_eth_revert(&short_data),
575            format!("0x{}", hex::encode(&short_data))
576        );
577
578        // Test unknown function selector
579        let mut unknown_selector = vec![0; EVM_FUNC_SELECTOR_LENGTH + EVM_WORD_LENGTH];
580        unknown_selector[0] = 0xAA;
581        unknown_selector[1] = 0xBB;
582        unknown_selector[2] = 0xCC;
583        unknown_selector[3] = 0xDD;
584        assert_eq!(
585            parse_eth_revert(&unknown_selector),
586            format!("0x{}", hex::encode(&unknown_selector))
587        );
588    }
589
590    #[test]
591    fn test_parse_error_revert_special_cases() {
592        // Test with empty error message
593        let data = create_error_data("");
594        assert_eq!(parse_error_revert(&data), "Error()");
595
596        // Test with special characters
597        let special = "Error message with special chars: !@#$%6^&*()_+{}|:<>!?";
598        let data = create_error_data(special);
599        assert_eq!(parse_error_revert(&data), format!("Error({special})"));
600
601        // Test with Unicode characters
602        let unicode = "Error with Unicode: 你好世界";
603        let data = create_error_data(unicode);
604        assert_eq!(parse_error_revert(&data), format!("Error({unicode})"));
605
606        // Test with invalid offset (points outside data)
607        let mut invalid_offset = create_error_data("Test");
608        // Modify offset to point outside available data
609        invalid_offset
610            .iter_mut()
611            .skip(24)
612            .take(8)
613            .for_each(|byte| *byte = 0xFF);
614        assert_eq!(
615            parse_error_revert(&invalid_offset),
616            format!("0x{}", hex::encode(&invalid_offset))
617        );
618
619        // Test with invalid length (exceeds available data)
620        let mut invalid_length = create_error_data("Test");
621        // Set offset to valid 32, but make length too large
622        invalid_length
623            .iter_mut()
624            .skip(32 + 24)
625            .take(8)
626            .for_each(|byte| *byte = 0xFF);
627        assert_eq!(
628            parse_error_revert(&invalid_length),
629            format!("0x{}", hex::encode(&invalid_length))
630        );
631
632        // Test with truncated data (not enough for string data)
633        let truncated = create_error_data("Test");
634        let truncated = &truncated[0..70]; // Cut off after length field
635        assert_eq!(
636            parse_error_revert(truncated),
637            format!("0x{}", hex::encode(truncated))
638        );
639
640        // Test with invalid UTF-8 in the string
641        let mut invalid_utf8 = create_error_data("Test string");
642        // Insert invalid UTF-8 sequence
643        let string_start = 32 + 32;
644        invalid_utf8[string_start + 2] = 0xFF;
645        assert_eq!(
646            parse_error_revert(&invalid_utf8),
647            format!("0x{}", hex::encode(&invalid_utf8))
648        );
649    }
650
651    #[test]
652    fn test_eth_revert_boundary_conditions() {
653        // Test with exactly minimum size data
654        let min_size = vec![0; EVM_FUNC_SELECTOR_LENGTH + EVM_WORD_LENGTH];
655        assert_eq!(
656            parse_eth_revert(&min_size),
657            format!("0x{}", hex::encode(&min_size))
658        );
659
660        // Test with exactly one byte less than minimum
661        let too_small = vec![0; EVM_FUNC_SELECTOR_LENGTH + EVM_WORD_LENGTH - 1];
662        assert_eq!(
663            parse_eth_revert(&too_small),
664            format!("0x{}", hex::encode(&too_small))
665        );
666    }
667
668    #[test]
669    fn test_decode_payload() {
670        // empty
671        let result = decode_payload(&RawBytes::default(), 0);
672        assert!(result.unwrap().0.is_empty());
673
674        // raw empty
675        let result = decode_payload(&RawBytes::default(), IPLD_RAW);
676        assert!(result.unwrap().0.is_empty());
677
678        // raw non-empty
679        let result = decode_payload(&RawBytes::new(vec![1]), IPLD_RAW);
680        assert_eq!(result.unwrap(), EthBytes(vec![1]));
681
682        // invalid cbor bytes
683        let result = decode_payload(&RawBytes::default(), DAG_CBOR);
684        assert!(result.is_err());
685
686        // valid cbor bytes
687        let encoded = cbor_bytes(vec![1]);
688
689        let result = decode_payload(&encoded, DAG_CBOR);
690        assert_eq!(result.unwrap(), EthBytes(vec![1]));
691
692        // regular cbor also works
693        let result = decode_payload(&encoded, CBOR);
694        assert_eq!(result.unwrap(), EthBytes(vec![1]));
695
696        // random codec should fail
697        let result = decode_payload(&RawBytes::default(), 42);
698        assert!(result.is_err());
699
700        // some payload taken from calibnet
701        assert_eq!(
702            decode_payload(
703                &RawBytes::new(
704                    hex::decode(
705                        "58200000000000000000000000000000000000000000000000000000000000002710"
706                    )
707                    .unwrap(),
708                ),
709                CBOR
710            )
711            .unwrap(),
712            EthBytes(vec![
713                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
714                0, 0, 39, 16,
715            ])
716        );
717
718        // identity
719        let result = decode_payload(&RawBytes::new(vec![1]), IDENTITY_HASH);
720        assert!(result.unwrap().0.is_empty());
721    }
722
723    fn cbor_bytes(inner: Vec<u8>) -> RawBytes {
724        RawBytes::new(cbor4ii::serde::to_vec(Vec::new(), &Value::Bytes(inner)).unwrap())
725    }
726
727    // Undecodable input is the expected non-Ethereum case; Lotus returns empty data and reason.
728    #[rstest]
729    #[case(RawBytes::new(vec![0x18]), vec![], "")]
730    #[case(cbor_bytes(vec![]), vec![], "none")]
731    #[case(
732        cbor_bytes(create_error_data("boom")),
733        create_error_data("boom"),
734        "Error(boom)"
735    )]
736    #[case(
737        cbor_bytes(create_panic_data(0x01)),
738        create_panic_data(0x01),
739        "Assert()"
740    )]
741    // Too short for a selector+word, so the reason is the raw hex passthrough.
742    #[case(cbor_bytes(vec![0xde, 0xad, 0xbe, 0xef]), vec![0xde, 0xad, 0xbe, 0xef], "0xdeadbeef")]
743    fn test_decode_revert_reason(
744        #[case] input: RawBytes,
745        #[case] expected_data: Vec<u8>,
746        #[case] expected_reason: &str,
747    ) {
748        let (data, reason) = decode_revert_reason(input);
749        assert_eq!(data, expected_data);
750        assert_eq!(reason, expected_reason);
751    }
752}