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