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 super::utils::decode_revert_reason;
5use crate::rpc::error::RpcErrorData;
6use crate::shim::clock::ChainEpoch;
7use crate::shim::error::ExitCode;
8use crate::utils::encoding::hex;
9use fvm_ipld_encoding::RawBytes;
10use serde::Serialize;
11use std::fmt::Debug;
12use thiserror::Error;
13
14pub const OUT_OF_GAS_CODE: i32 = 2;
15/// This error indicates that the execution reverted while executing the message.
16/// 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.
17pub const EXECUTION_REVERTED_CODE: i32 = 3;
18/// This error indicates that the block range provided in the RPC exceeds the configured maximum
19/// It was introduced in EIP-1474
20pub const LIMIT_EXCEEDED_CODE: i32 = -32005;
21/// Matches Lotus's `ENullRound` (`jsonrpc.FirstUserCode + 10` = `12`).
22pub const NULL_ROUND_CODE: i32 = 12;
23
24#[derive(Clone, Debug, Error, Serialize)]
25pub enum EthErrors {
26    #[error("call ran out of gas")]
27    OutOfGas,
28    #[error("{message}")]
29    ExecutionReverted { message: String, data: String },
30    #[error("{message}")]
31    BlockRangeExceeded {
32        max: i64,
33        given: i64,
34        message: String,
35    },
36    #[error("events for the requested block are not yet available")]
37    EventsNotYetAvailable,
38    #[error("requested epoch was a null round ({epoch})")]
39    NullRound { epoch: ChainEpoch },
40}
41
42impl EthErrors {
43    /// Create a new ExecutionReverted error with formatted message
44    pub fn execution_reverted(exit_code: ExitCode, reason: &str, error: &str, data: &[u8]) -> Self {
45        let revert_reason = if reason.is_empty() {
46            String::new()
47        } else {
48            format!(", revert reason=[{reason}]")
49        };
50
51        Self::ExecutionReverted {
52            message: format!(
53                "message execution failed (exit=[{exit_code}]{revert_reason}, vm error=[{error}])"
54            ),
55            data: hex::encode_prefixed(data),
56        }
57    }
58
59    /// Builds an eth `ExecutionReverted` (code 3) from a failed message's exit code and return
60    /// payload, decoding the revert reason and data.
61    pub fn execution_reverted_from_result(
62        exit_code: impl Into<ExitCode>,
63        return_data: RawBytes,
64        vm_error: &str,
65    ) -> Self {
66        let (data, reason) = decode_revert_reason(return_data);
67        Self::execution_reverted(exit_code.into(), &reason, vm_error, &data)
68    }
69
70    /// Prepends `prefix` to the message, keeping the code and data. Needed because the RPC layer
71    /// rebuilds the wire message from the typed error alone, dropping any `anyhow` context.
72    #[must_use]
73    pub fn with_message_prefix(mut self, prefix: &str) -> Self {
74        match &mut self {
75            Self::ExecutionReverted { message, .. } | Self::BlockRangeExceeded { message, .. } => {
76                *message = format!("{prefix}: {message}");
77            }
78            Self::OutOfGas | Self::EventsNotYetAvailable | Self::NullRound { .. } => {}
79        }
80        self
81    }
82
83    pub fn limit_exceeded(max_block_range: i64, given: i64) -> Self {
84        Self::BlockRangeExceeded {
85            max: max_block_range,
86            given,
87            message: format!("block range exceeds maximum of {max_block_range} (got {given})"),
88        }
89    }
90
91    /// Message matches Lotus's `ErrNullRound` verbatim.
92    pub fn null_round(epoch: ChainEpoch) -> Self {
93        Self::NullRound { epoch }
94    }
95}
96
97impl RpcErrorData for EthErrors {
98    fn error_code(&self) -> Option<i32> {
99        match self {
100            EthErrors::OutOfGas => Some(OUT_OF_GAS_CODE),
101            EthErrors::ExecutionReverted { .. } => Some(EXECUTION_REVERTED_CODE),
102            EthErrors::BlockRangeExceeded { .. } => Some(LIMIT_EXCEEDED_CODE),
103            EthErrors::EventsNotYetAvailable => None,
104            EthErrors::NullRound { .. } => Some(NULL_ROUND_CODE),
105        }
106    }
107
108    fn error_message(&self) -> Option<String> {
109        match self {
110            EthErrors::OutOfGas => Some(self.to_string()),
111            EthErrors::ExecutionReverted { message, .. } => Some(message.clone()),
112            EthErrors::BlockRangeExceeded { message, .. } => Some(message.clone()),
113            EthErrors::EventsNotYetAvailable => Some(self.to_string()),
114            EthErrors::NullRound { .. } => Some(self.to_string()),
115        }
116    }
117
118    fn error_data(&self) -> Option<serde_json::Value> {
119        match self {
120            EthErrors::ExecutionReverted { data, .. } => {
121                Some(serde_json::Value::String(data.clone()))
122            }
123            EthErrors::OutOfGas
124            | EthErrors::BlockRangeExceeded { .. }
125            | EthErrors::EventsNotYetAvailable => None,
126            // Lotus sends the epoch as a bare JSON number.
127            EthErrors::NullRound { epoch } => Some(serde_json::Value::from(*epoch)),
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::rpc::error::ServerError;
136
137    #[test]
138    fn test_block_range_exceeded_converts_to_server_error_with_correct_code() {
139        let err = EthErrors::limit_exceeded(100, 500);
140        let server_err: ServerError = err.into();
141
142        assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
143        assert_eq!(
144            server_err.message(),
145            "block range exceeds maximum of 100 (got 500)"
146        );
147    }
148
149    #[test]
150    fn test_block_range_exceeded_via_anyhow_preserves_code() {
151        let eth_err = EthErrors::limit_exceeded(2880, 5000);
152        let anyhow_err: anyhow::Error = eth_err.into();
153        let server_err: ServerError = anyhow_err.into();
154
155        assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
156        assert_eq!(
157            server_err.message(),
158            "block range exceeds maximum of 2880 (got 5000)"
159        );
160    }
161
162    #[test]
163    fn test_null_round_converts_to_server_error_matching_lotus() {
164        let err = EthErrors::null_round(3847253);
165        let server_err: ServerError = err.into();
166
167        // Must match Lotus's `ErrNullRound` exactly: code 12, message, numeric data.
168        assert_eq!(server_err.code(), NULL_ROUND_CODE);
169        assert_eq!(
170            server_err.message(),
171            "requested epoch was a null round (3847253)"
172        );
173        assert_eq!(
174            server_err.data().map(|d| d.to_string()),
175            Some("3847253".to_string())
176        );
177    }
178
179    #[test]
180    fn test_null_round_via_anyhow_preserves_code() {
181        let anyhow_err: anyhow::Error = EthErrors::null_round(42).into();
182        let server_err: ServerError = anyhow_err.into();
183
184        assert_eq!(server_err.code(), NULL_ROUND_CODE);
185        assert_eq!(
186            server_err.message(),
187            "requested epoch was a null round (42)"
188        );
189    }
190
191    #[test]
192    fn test_events_not_yet_available_converts_to_server_error() {
193        let err = EthErrors::EventsNotYetAvailable;
194        let server_err: ServerError = err.into();
195
196        // No specific RPC error code is assigned; falls back to default.
197        assert_eq!(
198            server_err.message(),
199            "events for the requested block are not yet available"
200        );
201    }
202
203    #[test]
204    fn test_with_message_prefix_prepends_and_preserves_code_and_data() {
205        let err = EthErrors::execution_reverted(
206            ExitCode::from(33u32),
207            "boom",
208            "backtrace",
209            &[0xde, 0xad],
210        )
211        .with_message_prefix("gas search failed");
212        let server_err: ServerError = err.into();
213
214        assert_eq!(server_err.code(), EXECUTION_REVERTED_CODE);
215        assert_eq!(
216            server_err.message(),
217            "gas search failed: message execution failed (exit=[33], revert reason=[boom], vm error=[backtrace])"
218        );
219        assert_eq!(
220            server_err.data().map(|d| d.to_string()),
221            Some("\"0xdead\"".to_string())
222        );
223    }
224
225    #[test]
226    fn test_with_message_prefix_is_a_noop_for_messageless_variants() {
227        let err = EthErrors::null_round(7).with_message_prefix("ignored");
228        assert_eq!(err.to_string(), "requested epoch was a null round (7)");
229    }
230
231    #[test]
232    fn test_out_of_gas_converts_to_server_error_matching_lotus() {
233        let err = EthErrors::OutOfGas;
234        let server_err: ServerError = err.into();
235
236        assert_eq!(server_err.code(), OUT_OF_GAS_CODE);
237        assert_eq!(server_err.message(), "call ran out of gas");
238    }
239}