forest-filecoin 0.38.0

Rust Filecoin implementation.
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use super::utils::decode_revert_reason;
use crate::rpc::error::RpcErrorData;
use crate::shim::clock::ChainEpoch;
use crate::shim::error::ExitCode;
use crate::shim::executor::ApplyRet;
use crate::utils::encoding::hex;
use fvm_ipld_encoding::RawBytes;
use serde::Serialize;
use std::fmt::Debug;
use thiserror::Error;

pub const OUT_OF_GAS_CODE: i32 = 2;
/// This error indicates that the execution reverted while executing the message.
/// Error code 3 was introduced in geth v1.9.15 and is now expected by most Ethereum ecosystem tooling for automatic ABI decoding of revert reasons from the error data field.
pub const EXECUTION_REVERTED_CODE: i32 = 3;
/// This error indicates that the block range provided in the RPC exceeds the configured maximum
/// It was introduced in EIP-1474
pub const LIMIT_EXCEEDED_CODE: i32 = -32005;
/// Matches Lotus's `ENullRound` (`jsonrpc.FirstUserCode + 10` = `12`).
pub const NULL_ROUND_CODE: i32 = 12;
/// "Invalid input" in [EIP-1474](https://github.com/ethereum/EIPs/blob/ac912ca6a9685590345dd8e5736cda75976d0131/EIPS/eip-1474.md#L43).
pub const INVALID_INPUT_CODE: i32 = -32000;
/// "Transaction rejected" in [EIP-1474](https://github.com/ethereum/EIPs/blob/ac912ca6a9685590345dd8e5736cda75976d0131/EIPS/eip-1474.md#L46).
pub const TRANSACTION_REJECTED_CODE: i32 = -32003;

#[derive(Clone, Debug, Error, Serialize)]
pub enum EthErrors {
    #[error("call ran out of gas")]
    OutOfGas,
    #[error("{message}")]
    ExecutionReverted { message: String, data: String },
    #[error("{message}")]
    BlockRangeExceeded {
        max: i64,
        given: i64,
        message: String,
    },
    #[error("events for the requested block are not yet available")]
    EventsNotYetAvailable,
    #[error("requested epoch was a null round ({epoch})")]
    NullRound { epoch: ChainEpoch },
    /// The caller-supplied gas limit is below the message inclusion cost.
    #[error("gas required exceeds allowance ({gas_limit})")]
    GasRequiredExceedsAllowance { gas_limit: u64 },
    /// The call runs out of gas, or fails, within the caller-supplied gas limit but succeeds with more.
    #[error("out of gas: gas required exceeds: {gas_limit}")]
    InsufficientGasLimit { gas_limit: u64 },
}

impl EthErrors {
    /// Create a new ExecutionReverted error with formatted message
    pub fn execution_reverted(exit_code: ExitCode, reason: &str, error: &str, data: &[u8]) -> Self {
        let revert_reason = if reason.is_empty() {
            String::new()
        } else {
            format!(", revert reason=[{reason}]")
        };

        Self::ExecutionReverted {
            message: format!(
                "message execution failed (exit=[{exit_code}]{revert_reason}, vm error=[{error}])"
            ),
            data: hex::encode_prefixed(data),
        }
    }

    /// Builds an eth `ExecutionReverted` (code 3) from a failed message's exit code and return
    /// payload, decoding the revert reason and data.
    pub fn execution_reverted_from_result(
        exit_code: impl Into<ExitCode>,
        return_data: RawBytes,
        vm_error: &str,
    ) -> Self {
        let (data, reason) = decode_revert_reason(return_data);
        Self::execution_reverted(exit_code.into(), &reason, vm_error, &data)
    }

    /// [`Self::execution_reverted_from_result`] for a failed [`ApplyRet`].
    pub fn execution_reverted_from_apply_ret(apply_ret: &ApplyRet) -> Self {
        Self::execution_reverted_from_result(
            apply_ret.exit_code(),
            apply_ret.return_data(),
            &apply_ret.failure_info().unwrap_or_default(),
        )
    }

    pub fn limit_exceeded(max_block_range: i64, given: i64) -> Self {
        Self::BlockRangeExceeded {
            max: max_block_range,
            given,
            message: format!("block range exceeds maximum of {max_block_range} (got {given})"),
        }
    }

    /// Message matches Lotus's `ErrNullRound` verbatim.
    pub fn null_round(epoch: ChainEpoch) -> Self {
        Self::NullRound { epoch }
    }
}

impl RpcErrorData for EthErrors {
    fn error_code(&self) -> Option<i32> {
        match self {
            EthErrors::OutOfGas => Some(OUT_OF_GAS_CODE),
            EthErrors::ExecutionReverted { .. } => Some(EXECUTION_REVERTED_CODE),
            EthErrors::BlockRangeExceeded { .. } => Some(LIMIT_EXCEEDED_CODE),
            EthErrors::EventsNotYetAvailable => None,
            EthErrors::NullRound { .. } => Some(NULL_ROUND_CODE),
            EthErrors::GasRequiredExceedsAllowance { .. } => Some(INVALID_INPUT_CODE),
            EthErrors::InsufficientGasLimit { .. } => Some(TRANSACTION_REJECTED_CODE),
        }
    }

    fn error_message(&self) -> Option<String> {
        match self {
            EthErrors::OutOfGas => Some(self.to_string()),
            EthErrors::ExecutionReverted { message, .. } => Some(message.clone()),
            EthErrors::BlockRangeExceeded { message, .. } => Some(message.clone()),
            EthErrors::EventsNotYetAvailable => Some(self.to_string()),
            EthErrors::NullRound { .. } => Some(self.to_string()),
            EthErrors::GasRequiredExceedsAllowance { .. }
            | EthErrors::InsufficientGasLimit { .. } => Some(self.to_string()),
        }
    }

    fn error_data(&self) -> Option<serde_json::Value> {
        match self {
            EthErrors::ExecutionReverted { data, .. } => {
                Some(serde_json::Value::String(data.clone()))
            }
            EthErrors::OutOfGas
            | EthErrors::BlockRangeExceeded { .. }
            | EthErrors::EventsNotYetAvailable
            | EthErrors::GasRequiredExceedsAllowance { .. }
            | EthErrors::InsufficientGasLimit { .. } => None,
            // Lotus sends the epoch as a bare JSON number.
            EthErrors::NullRound { epoch } => Some(serde_json::Value::from(*epoch)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rpc::error::ServerError;

    #[test]
    fn test_block_range_exceeded_converts_to_server_error_with_correct_code() {
        let err = EthErrors::limit_exceeded(100, 500);
        let server_err: ServerError = err.into();

        assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
        assert_eq!(
            server_err.message(),
            "block range exceeds maximum of 100 (got 500)"
        );
    }

    #[test]
    fn test_block_range_exceeded_via_anyhow_preserves_code() {
        let eth_err = EthErrors::limit_exceeded(2880, 5000);
        let anyhow_err: anyhow::Error = eth_err.into();
        let server_err: ServerError = anyhow_err.into();

        assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
        assert_eq!(
            server_err.message(),
            "block range exceeds maximum of 2880 (got 5000)"
        );
    }

    #[test]
    fn test_null_round_converts_to_server_error_matching_lotus() {
        let err = EthErrors::null_round(3847253);
        let server_err: ServerError = err.into();

        // Must match Lotus's `ErrNullRound` exactly: code 12, message, numeric data.
        assert_eq!(server_err.code(), NULL_ROUND_CODE);
        assert_eq!(
            server_err.message(),
            "requested epoch was a null round (3847253)"
        );
        assert_eq!(
            server_err.data().map(|d| d.to_string()),
            Some("3847253".to_string())
        );
    }

    #[test]
    fn test_null_round_via_anyhow_preserves_code() {
        let anyhow_err: anyhow::Error = EthErrors::null_round(42).into();
        let server_err: ServerError = anyhow_err.into();

        assert_eq!(server_err.code(), NULL_ROUND_CODE);
        assert_eq!(
            server_err.message(),
            "requested epoch was a null round (42)"
        );
    }

    #[test]
    fn test_events_not_yet_available_converts_to_server_error() {
        let err = EthErrors::EventsNotYetAvailable;
        let server_err: ServerError = err.into();

        // No specific RPC error code is assigned; falls back to default.
        assert_eq!(
            server_err.message(),
            "events for the requested block are not yet available"
        );
    }

    #[test]
    fn test_out_of_gas_converts_to_server_error_matching_lotus() {
        let err = EthErrors::OutOfGas;
        let server_err: ServerError = err.into();

        assert_eq!(server_err.code(), OUT_OF_GAS_CODE);
        assert_eq!(server_err.message(), "call ran out of gas");
    }

    #[test]
    fn test_gas_required_exceeds_allowance_converts_to_server_error() {
        let server_err: ServerError =
            EthErrors::GasRequiredExceedsAllowance { gas_limit: 1000 }.into();

        assert_eq!(server_err.code(), INVALID_INPUT_CODE);
        assert_eq!(
            server_err.message(),
            "gas required exceeds allowance (1000)"
        );
        assert!(server_err.data().is_none());
    }

    #[test]
    fn test_insufficient_gas_limit_converts_to_server_error() {
        let server_err: ServerError = EthErrors::InsufficientGasLimit { gas_limit: 25000 }.into();

        assert_eq!(server_err.code(), TRANSACTION_REJECTED_CODE);
        assert_eq!(
            server_err.message(),
            "out of gas: gas required exceeds: 25000"
        );
        assert!(server_err.data().is_none());
    }
}