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    /// Prepends `prefix` to the message, keeping the code and data. Needed because the RPC layer
55    /// rebuilds the wire message from the typed error alone, dropping any `anyhow` context.
56    #[must_use]
57    pub fn with_message_prefix(mut self, prefix: &str) -> Self {
58        match &mut self {
59            Self::ExecutionReverted { message, .. } | Self::BlockRangeExceeded { message, .. } => {
60                *message = format!("{prefix}: {message}");
61            }
62            Self::EventsNotYetAvailable | Self::NullRound { .. } => {}
63        }
64        self
65    }
66
67    pub fn limit_exceeded(max_block_range: i64, given: i64) -> Self {
68        Self::BlockRangeExceeded {
69            max: max_block_range,
70            given,
71            message: format!("block range exceeds maximum of {max_block_range} (got {given})"),
72        }
73    }
74
75    /// Message matches Lotus's `ErrNullRound` verbatim.
76    pub fn null_round(epoch: ChainEpoch) -> Self {
77        Self::NullRound { epoch }
78    }
79}
80
81impl RpcErrorData for EthErrors {
82    fn error_code(&self) -> Option<i32> {
83        match self {
84            EthErrors::ExecutionReverted { .. } => Some(EXECUTION_REVERTED_CODE),
85            EthErrors::BlockRangeExceeded { .. } => Some(LIMIT_EXCEEDED_CODE),
86            EthErrors::EventsNotYetAvailable => None,
87            EthErrors::NullRound { .. } => Some(NULL_ROUND_CODE),
88        }
89    }
90
91    fn error_message(&self) -> Option<String> {
92        match self {
93            EthErrors::ExecutionReverted { message, .. } => Some(message.clone()),
94            EthErrors::BlockRangeExceeded { message, .. } => Some(message.clone()),
95            EthErrors::EventsNotYetAvailable => Some(self.to_string()),
96            EthErrors::NullRound { .. } => Some(self.to_string()),
97        }
98    }
99
100    fn error_data(&self) -> Option<serde_json::Value> {
101        match self {
102            EthErrors::ExecutionReverted { data, .. } => {
103                Some(serde_json::Value::String(data.clone()))
104            }
105            EthErrors::BlockRangeExceeded { .. } => None,
106            EthErrors::EventsNotYetAvailable => None,
107            // Lotus sends the epoch as a bare JSON number.
108            EthErrors::NullRound { epoch } => Some(serde_json::Value::from(*epoch)),
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::rpc::error::ServerError;
117
118    #[test]
119    fn test_block_range_exceeded_converts_to_server_error_with_correct_code() {
120        let err = EthErrors::limit_exceeded(100, 500);
121        let server_err: ServerError = 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 100 (got 500)"
127        );
128    }
129
130    #[test]
131    fn test_block_range_exceeded_via_anyhow_preserves_code() {
132        let eth_err = EthErrors::limit_exceeded(2880, 5000);
133        let anyhow_err: anyhow::Error = eth_err.into();
134        let server_err: ServerError = anyhow_err.into();
135
136        assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
137        assert_eq!(
138            server_err.message(),
139            "block range exceeds maximum of 2880 (got 5000)"
140        );
141    }
142
143    #[test]
144    fn test_null_round_converts_to_server_error_matching_lotus() {
145        let err = EthErrors::null_round(3847253);
146        let server_err: ServerError = err.into();
147
148        // Must match Lotus's `ErrNullRound` exactly: code 12, message, numeric data.
149        assert_eq!(server_err.code(), NULL_ROUND_CODE);
150        assert_eq!(
151            server_err.message(),
152            "requested epoch was a null round (3847253)"
153        );
154        assert_eq!(
155            server_err.data().map(|d| d.to_string()),
156            Some("3847253".to_string())
157        );
158    }
159
160    #[test]
161    fn test_null_round_via_anyhow_preserves_code() {
162        let anyhow_err: anyhow::Error = EthErrors::null_round(42).into();
163        let server_err: ServerError = anyhow_err.into();
164
165        assert_eq!(server_err.code(), NULL_ROUND_CODE);
166        assert_eq!(
167            server_err.message(),
168            "requested epoch was a null round (42)"
169        );
170    }
171
172    #[test]
173    fn test_events_not_yet_available_converts_to_server_error() {
174        let err = EthErrors::EventsNotYetAvailable;
175        let server_err: ServerError = err.into();
176
177        // No specific RPC error code is assigned; falls back to default.
178        assert_eq!(
179            server_err.message(),
180            "events for the requested block are not yet available"
181        );
182    }
183
184    #[test]
185    fn test_with_message_prefix_prepends_and_preserves_code_and_data() {
186        let err = EthErrors::execution_reverted(
187            ExitCode::from(33u32),
188            "boom",
189            "backtrace",
190            &[0xde, 0xad],
191        )
192        .with_message_prefix("gas search failed");
193        let server_err: ServerError = err.into();
194
195        assert_eq!(server_err.code(), EXECUTION_REVERTED_CODE);
196        assert_eq!(
197            server_err.message(),
198            "gas search failed: message execution failed (exit=[33], revert reason=[boom], vm error=[backtrace])"
199        );
200        assert_eq!(
201            server_err.data().map(|d| d.to_string()),
202            Some("\"0xdead\"".to_string())
203        );
204    }
205
206    #[test]
207    fn test_with_message_prefix_is_a_noop_for_messageless_variants() {
208        let err = EthErrors::null_round(7).with_message_prefix("ignored");
209        assert_eq!(err.to_string(), "requested epoch was a null round (7)");
210    }
211}