Skip to main content

forest/rpc/methods/eth/
errors.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::rpc::error::RpcErrorData;
5use crate::shim::clock::ChainEpoch;
6use crate::shim::error::ExitCode;
7use crate::utils::encoding::hex;
8use serde::Serialize;
9use std::fmt::Debug;
10use thiserror::Error;
11
12/// This error indicates that the execution reverted while executing the message.
13/// 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.
14pub const EXECUTION_REVERTED_CODE: i32 = 3;
15/// This error indicates that the block range provided in the RPC exceeds the configured maximum
16/// It was introduced in EIP-1474
17pub const LIMIT_EXCEEDED_CODE: i32 = -32005;
18/// Matches Lotus's `ENullRound` (`jsonrpc.FirstUserCode + 10` = `12`).
19pub const NULL_ROUND_CODE: i32 = 12;
20
21#[derive(Clone, Debug, Error, Serialize)]
22pub enum EthErrors {
23    #[error("{message}")]
24    ExecutionReverted { message: String, data: String },
25    #[error("{message}")]
26    BlockRangeExceeded {
27        max: i64,
28        given: i64,
29        message: String,
30    },
31    #[error("events for the requested block are not yet available")]
32    EventsNotYetAvailable,
33    #[error("requested epoch was a null round ({epoch})")]
34    NullRound { epoch: ChainEpoch },
35}
36
37impl EthErrors {
38    /// Create a new ExecutionReverted error with formatted message
39    pub fn execution_reverted(exit_code: ExitCode, reason: &str, error: &str, data: &[u8]) -> Self {
40        let revert_reason = if reason.is_empty() {
41            String::new()
42        } else {
43            format!(", revert reason=[{reason}]")
44        };
45
46        Self::ExecutionReverted {
47            message: format!(
48                "message execution failed (exit=[{exit_code}]{revert_reason}, vm error=[{error}])"
49            ),
50            data: hex::encode_prefixed(data),
51        }
52    }
53
54    pub fn limit_exceeded(max_block_range: i64, given: i64) -> Self {
55        Self::BlockRangeExceeded {
56            max: max_block_range,
57            given,
58            message: format!("block range exceeds maximum of {max_block_range} (got {given})"),
59        }
60    }
61
62    /// Message matches Lotus's `ErrNullRound` verbatim.
63    pub fn null_round(epoch: ChainEpoch) -> Self {
64        Self::NullRound { epoch }
65    }
66}
67
68impl RpcErrorData for EthErrors {
69    fn error_code(&self) -> Option<i32> {
70        match self {
71            EthErrors::ExecutionReverted { .. } => Some(EXECUTION_REVERTED_CODE),
72            EthErrors::BlockRangeExceeded { .. } => Some(LIMIT_EXCEEDED_CODE),
73            EthErrors::EventsNotYetAvailable => None,
74            EthErrors::NullRound { .. } => Some(NULL_ROUND_CODE),
75        }
76    }
77
78    fn error_message(&self) -> Option<String> {
79        match self {
80            EthErrors::ExecutionReverted { message, .. } => Some(message.clone()),
81            EthErrors::BlockRangeExceeded { message, .. } => Some(message.clone()),
82            EthErrors::EventsNotYetAvailable => Some(self.to_string()),
83            EthErrors::NullRound { .. } => Some(self.to_string()),
84        }
85    }
86
87    fn error_data(&self) -> Option<serde_json::Value> {
88        match self {
89            EthErrors::ExecutionReverted { data, .. } => {
90                Some(serde_json::Value::String(data.clone()))
91            }
92            EthErrors::BlockRangeExceeded { .. } => None,
93            EthErrors::EventsNotYetAvailable => None,
94            // Lotus sends the epoch as a bare JSON number.
95            EthErrors::NullRound { epoch } => Some(serde_json::Value::from(*epoch)),
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::rpc::error::ServerError;
104
105    #[test]
106    fn test_block_range_exceeded_converts_to_server_error_with_correct_code() {
107        let err = EthErrors::limit_exceeded(100, 500);
108        let server_err: ServerError = err.into();
109
110        assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
111        assert_eq!(
112            server_err.message(),
113            "block range exceeds maximum of 100 (got 500)"
114        );
115    }
116
117    #[test]
118    fn test_block_range_exceeded_via_anyhow_preserves_code() {
119        let eth_err = EthErrors::limit_exceeded(2880, 5000);
120        let anyhow_err: anyhow::Error = eth_err.into();
121        let server_err: ServerError = anyhow_err.into();
122
123        assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
124        assert_eq!(
125            server_err.message(),
126            "block range exceeds maximum of 2880 (got 5000)"
127        );
128    }
129
130    #[test]
131    fn test_null_round_converts_to_server_error_matching_lotus() {
132        let err = EthErrors::null_round(3847253);
133        let server_err: ServerError = err.into();
134
135        // Must match Lotus's `ErrNullRound` exactly: code 12, message, numeric data.
136        assert_eq!(server_err.code(), NULL_ROUND_CODE);
137        assert_eq!(
138            server_err.message(),
139            "requested epoch was a null round (3847253)"
140        );
141        assert_eq!(
142            server_err.data().map(|d| d.to_string()),
143            Some("3847253".to_string())
144        );
145    }
146
147    #[test]
148    fn test_null_round_via_anyhow_preserves_code() {
149        let anyhow_err: anyhow::Error = EthErrors::null_round(42).into();
150        let server_err: ServerError = anyhow_err.into();
151
152        assert_eq!(server_err.code(), NULL_ROUND_CODE);
153        assert_eq!(
154            server_err.message(),
155            "requested epoch was a null round (42)"
156        );
157    }
158
159    #[test]
160    fn test_events_not_yet_available_converts_to_server_error() {
161        let err = EthErrors::EventsNotYetAvailable;
162        let server_err: ServerError = err.into();
163
164        // No specific RPC error code is assigned; falls back to default.
165        assert_eq!(
166            server_err.message(),
167            "events for the requested block are not yet available"
168        );
169    }
170}