1use alloy_primitives::B256;
2use alloy_rpc_types_engine::{
3 ForkchoiceUpdateError, INVALID_FORK_CHOICE_STATE_ERROR, INVALID_FORK_CHOICE_STATE_ERROR_MSG,
4 INVALID_PAYLOAD_ATTRIBUTES_ERROR, INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG, TOO_DEEP_REORG_ERROR,
5 TOO_DEEP_REORG_ERROR_MSG,
6};
7use jsonrpsee_types::error::{
8 INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE, INVALID_PARAMS_MSG, SERVER_ERROR_MSG,
9};
10use reth_engine_primitives::{BeaconForkChoiceUpdateError, BeaconOnNewPayloadError};
11use reth_payload_builder_primitives::PayloadBuilderError;
12use reth_payload_primitives::{EngineObjectValidationError, VersionSpecificValidationError};
13use thiserror::Error;
14
15pub type EngineApiResult<Ok> = Result<Ok, EngineApiError>;
17
18pub const UNSUPPORTED_FORK_CODE: i32 = -38005;
20pub const UNKNOWN_PAYLOAD_CODE: i32 = -38001;
22pub const REQUEST_TOO_LARGE_CODE: i32 = -38004;
24
25const REQUEST_TOO_LARGE_MESSAGE: &str = "Too large request";
27
28#[derive(Error, Debug)]
33pub enum EngineApiError {
34 #[error("Unknown payload")]
37 UnknownPayload,
38 #[error("requested count too large: {len}")]
40 PayloadRequestTooLarge {
41 len: u64,
43 },
44 #[error("requested blob count too large: {len}")]
46 BlobRequestTooLarge {
47 len: usize,
49 },
50 #[error("invalid start ({start}) or count ({count})")]
52 InvalidBodiesRange {
53 start: u64,
55 count: u64,
57 },
58 #[error(
60 "invalid transition terminal block hash: \
61 execution: {execution:?}, consensus: {consensus}"
62 )]
63 TerminalBlockHash {
64 execution: Option<B256>,
66 consensus: B256,
68 },
69 #[error(transparent)]
71 ForkChoiceUpdate(#[from] BeaconForkChoiceUpdateError),
72 #[error(transparent)]
74 NewPayload(#[from] BeaconOnNewPayloadError),
75 #[error(transparent)]
77 Internal(#[from] Box<dyn core::error::Error + Send + Sync>),
78 #[error(transparent)]
80 GetPayloadError(#[from] PayloadBuilderError),
81 #[error(transparent)]
83 EngineObjectValidationError(#[from] EngineObjectValidationError),
84 #[error("requests hash cannot be accepted by the API without `--engine.accept-execution-requests-hash` flag")]
86 UnexpectedRequestsHash,
87 #[error("{0}")]
89 Other(jsonrpsee_types::ErrorObject<'static>),
90}
91
92impl EngineApiError {
93 pub const fn other(err: jsonrpsee_types::ErrorObject<'static>) -> Self {
95 Self::Other(err)
96 }
97}
98
99#[derive(serde::Serialize)]
102struct ErrorData {
103 err: String,
104}
105
106impl ErrorData {
107 #[inline]
108 fn new(err: impl std::fmt::Display) -> Self {
109 Self { err: err.to_string() }
110 }
111}
112
113impl From<EngineApiError> for jsonrpsee_types::error::ErrorObject<'static> {
114 fn from(error: EngineApiError) -> Self {
115 match error {
116 EngineApiError::EngineObjectValidationError(
121 EngineObjectValidationError::PayloadAttributes(
122 VersionSpecificValidationError::WithdrawalsNotSupportedInV1 |
123 VersionSpecificValidationError::NoWithdrawalsPostShanghai |
124 VersionSpecificValidationError::HasWithdrawalsPreShanghai |
125 VersionSpecificValidationError::BlockAccessListNotSupported |
126 VersionSpecificValidationError::NoBlockAccessListPostAmsterdam |
127 VersionSpecificValidationError::NoSlotNumberPostAmsterdam |
128 VersionSpecificValidationError::SlotNumberNotSupported,
129 ),
130 ) |
131 EngineApiError::UnexpectedRequestsHash => {
132 jsonrpsee_types::error::ErrorObject::owned(
135 INVALID_PAYLOAD_ATTRIBUTES_ERROR,
136 INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
137 Some(ErrorData::new(error)),
138 )
139 }
140 EngineApiError::InvalidBodiesRange { .. } |
141 EngineApiError::NewPayload(BeaconOnNewPayloadError::InvalidParams(_)) |
142 EngineApiError::EngineObjectValidationError(
143 EngineObjectValidationError::Payload(_) |
144 EngineObjectValidationError::InvalidParams(_),
145 ) => jsonrpsee_types::error::ErrorObject::owned(
146 INVALID_PARAMS_CODE,
147 INVALID_PARAMS_MSG,
148 Some(ErrorData::new(error)),
149 ),
150 EngineApiError::UnknownPayload => jsonrpsee_types::error::ErrorObject::owned(
151 UNKNOWN_PAYLOAD_CODE,
152 error.to_string(),
153 None::<()>,
154 ),
155 EngineApiError::PayloadRequestTooLarge { .. } |
156 EngineApiError::BlobRequestTooLarge { .. } => {
157 jsonrpsee_types::error::ErrorObject::owned(
158 REQUEST_TOO_LARGE_CODE,
159 REQUEST_TOO_LARGE_MESSAGE,
160 Some(ErrorData::new(error)),
161 )
162 }
163 EngineApiError::EngineObjectValidationError(
164 EngineObjectValidationError::PayloadAttributes(
165 VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3 |
166 VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun,
167 ),
168 ) => jsonrpsee_types::error::ErrorObject::owned(
169 INVALID_PAYLOAD_ATTRIBUTES_ERROR,
170 INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
171 Some(ErrorData::new(error)),
172 ),
173 EngineApiError::EngineObjectValidationError(
174 EngineObjectValidationError::UnsupportedFork,
175 ) => jsonrpsee_types::error::ErrorObject::owned(
176 UNSUPPORTED_FORK_CODE,
177 error.to_string(),
178 None::<()>,
179 ),
180 EngineApiError::ForkChoiceUpdate(ref err) => match err {
182 BeaconForkChoiceUpdateError::ForkchoiceUpdateError(err) => match err {
183 ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes => {
184 jsonrpsee_types::error::ErrorObject::owned(
185 INVALID_PAYLOAD_ATTRIBUTES_ERROR,
186 INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
187 None::<()>,
188 )
189 }
190 ForkchoiceUpdateError::InvalidState |
191 ForkchoiceUpdateError::UnknownFinalBlock => {
192 jsonrpsee_types::error::ErrorObject::owned(
193 INVALID_FORK_CHOICE_STATE_ERROR,
194 INVALID_FORK_CHOICE_STATE_ERROR_MSG,
195 None::<()>,
196 )
197 }
198 ForkchoiceUpdateError::TooDeepReorg => {
199 jsonrpsee_types::error::ErrorObject::owned(
200 TOO_DEEP_REORG_ERROR,
201 TOO_DEEP_REORG_ERROR_MSG,
202 None::<()>,
203 )
204 }
205 #[allow(unreachable_patterns, clippy::needless_return)]
207 _ => {
208 return jsonrpsee_types::error::ErrorObject::owned(
209 INTERNAL_ERROR_CODE,
210 SERVER_ERROR_MSG,
211 Some(ErrorData::new(error)),
212 );
213 }
214 },
215 BeaconForkChoiceUpdateError::PayloadJob(
219 PayloadBuilderError::InvalidAttributes(_),
220 ) => jsonrpsee_types::error::ErrorObject::owned(
221 INVALID_PAYLOAD_ATTRIBUTES_ERROR,
222 INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
223 Some(ErrorData::new(error)),
224 ),
225 BeaconForkChoiceUpdateError::EngineUnavailable |
226 BeaconForkChoiceUpdateError::PayloadJob(_) |
227 BeaconForkChoiceUpdateError::Internal(_) => {
228 jsonrpsee_types::error::ErrorObject::owned(
229 INTERNAL_ERROR_CODE,
230 SERVER_ERROR_MSG,
231 Some(ErrorData::new(error)),
232 )
233 }
234 },
235 EngineApiError::TerminalBlockHash { .. } |
237 EngineApiError::NewPayload(_) |
238 EngineApiError::Internal(_) |
239 EngineApiError::GetPayloadError(_) => jsonrpsee_types::error::ErrorObject::owned(
240 INTERNAL_ERROR_CODE,
241 SERVER_ERROR_MSG,
242 Some(ErrorData::new(error)),
243 ),
244 EngineApiError::Other(err) => err,
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use alloy_rpc_types_engine::ForkchoiceUpdateError;
253 use reth_payload_primitives::InvalidPayloadAttributesError;
254 #[track_caller]
255 fn ensure_engine_rpc_error(
256 code: i32,
257 message: &str,
258 err: impl Into<jsonrpsee_types::error::ErrorObject<'static>>,
259 ) {
260 let err = err.into();
261 assert_eq!(err.code(), code);
262 assert_eq!(err.message(), message);
263 }
264
265 #[test]
268 fn engine_error_rpc_error_test() {
269 ensure_engine_rpc_error(
270 UNSUPPORTED_FORK_CODE,
271 "Unsupported fork",
272 EngineApiError::EngineObjectValidationError(
273 EngineObjectValidationError::UnsupportedFork,
274 ),
275 );
276
277 ensure_engine_rpc_error(
278 REQUEST_TOO_LARGE_CODE,
279 "Too large request",
280 EngineApiError::PayloadRequestTooLarge { len: 0 },
281 );
282
283 ensure_engine_rpc_error(
284 -38002,
285 "Invalid forkchoice state",
286 EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
287 ForkchoiceUpdateError::InvalidState,
288 )),
289 );
290
291 ensure_engine_rpc_error(
294 -38003,
295 "Invalid payload attributes",
296 EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
297 ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
298 )),
299 );
300
301 ensure_engine_rpc_error(
302 -38006,
303 "Too deep reorg",
304 EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
305 ForkchoiceUpdateError::TooDeepReorg,
306 )),
307 );
308
309 ensure_engine_rpc_error(
310 UNKNOWN_PAYLOAD_CODE,
311 "Unknown payload",
312 EngineApiError::UnknownPayload,
313 );
314
315 ensure_engine_rpc_error(
318 INVALID_PARAMS_CODE,
319 INVALID_PARAMS_MSG,
320 EngineApiError::NewPayload(BeaconOnNewPayloadError::InvalidParams(
321 "malformed payload parameters".into(),
322 )),
323 );
324
325 ensure_engine_rpc_error(
330 INVALID_PAYLOAD_ATTRIBUTES_ERROR,
331 INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
332 EngineApiError::EngineObjectValidationError(
333 EngineObjectValidationError::PayloadAttributes(
334 VersionSpecificValidationError::NoWithdrawalsPostShanghai,
335 ),
336 ),
337 );
338
339 ensure_engine_rpc_error(
340 INVALID_PARAMS_CODE,
341 INVALID_PARAMS_MSG,
342 EngineApiError::EngineObjectValidationError(EngineObjectValidationError::Payload(
343 VersionSpecificValidationError::NoWithdrawalsPostShanghai,
344 )),
345 );
346
347 ensure_engine_rpc_error(
348 INVALID_PARAMS_CODE,
349 INVALID_PARAMS_MSG,
350 EngineApiError::EngineObjectValidationError(EngineObjectValidationError::Payload(
351 VersionSpecificValidationError::HasWithdrawalsPreShanghai,
352 )),
353 );
354
355 ensure_engine_rpc_error(
356 INVALID_PAYLOAD_ATTRIBUTES_ERROR,
357 INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
358 EngineApiError::EngineObjectValidationError(
359 EngineObjectValidationError::PayloadAttributes(
360 VersionSpecificValidationError::HasWithdrawalsPreShanghai,
361 ),
362 ),
363 );
364
365 ensure_engine_rpc_error(
367 INVALID_PAYLOAD_ATTRIBUTES_ERROR,
368 INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
369 EngineApiError::EngineObjectValidationError(
370 EngineObjectValidationError::PayloadAttributes(
371 VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
372 ),
373 ),
374 );
375 }
376
377 fn payload_job_error(err: PayloadBuilderError) -> jsonrpsee_types::error::ErrorObject<'static> {
378 EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::PayloadJob(err)).into()
379 }
380
381 #[test]
382 fn payload_job_rejecting_the_attributes_is_invalid_payload_attributes() {
383 let err = payload_job_error(PayloadBuilderError::invalid_attributes(
384 InvalidPayloadAttributesError::InvalidTimestamp,
385 ));
386
387 assert_eq!(err.code(), INVALID_PAYLOAD_ATTRIBUTES_ERROR);
388 assert_eq!(err.message(), INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG);
389 assert_eq!(
390 err.data().unwrap().get(),
391 r#"{"err":"failed to start payload job: invalid payload attributes: invalid timestamp"}"#
392 );
393 }
394
395 #[test]
396 fn payload_job_failing_on_this_node_is_an_internal_error() {
397 let node_local = [
398 PayloadBuilderError::MissingParentHeader(B256::ZERO),
399 PayloadBuilderError::ChannelClosed,
400 PayloadBuilderError::other(std::io::Error::other("other")),
401 ];
402
403 for payload_job in node_local {
404 let message = payload_job.to_string();
405 let err = payload_job_error(payload_job);
406 assert_eq!(err.code(), INTERNAL_ERROR_CODE, "{message}");
407 assert_eq!(err.message(), SERVER_ERROR_MSG, "{message}");
408 assert_eq!(
409 err.data().unwrap().get(),
410 format!(r#"{{"err":"failed to start payload job: {message}"}}"#)
411 );
412 }
413 }
414}