forest/rpc/methods/eth/
errors.rs1use 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;
15pub const EXECUTION_REVERTED_CODE: i32 = 3;
18pub const LIMIT_EXCEEDED_CODE: i32 = -32005;
21pub 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 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 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 #[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 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 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 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 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}