Skip to main content

reth_rpc_engine_api/
error.rs

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
15/// The Engine API result type
16pub type EngineApiResult<Ok> = Result<Ok, EngineApiError>;
17
18/// Payload unsupported fork code.
19pub const UNSUPPORTED_FORK_CODE: i32 = -38005;
20/// Payload unknown error code.
21pub const UNKNOWN_PAYLOAD_CODE: i32 = -38001;
22/// Request too large error code.
23pub const REQUEST_TOO_LARGE_CODE: i32 = -38004;
24
25/// Error message for the request too large error.
26const REQUEST_TOO_LARGE_MESSAGE: &str = "Too large request";
27
28/// Error returned by [`EngineApi`][crate::EngineApi]
29///
30/// Note: This is a high-fidelity error type which can be converted to an RPC error that adheres to
31/// the [Engine API spec](https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors).
32#[derive(Error, Debug)]
33pub enum EngineApiError {
34    // **IMPORTANT**: keep error messages in sync with the Engine API spec linked above.
35    /// Payload does not exist / is not available.
36    #[error("Unknown payload")]
37    UnknownPayload,
38    /// The payload body request length is too large.
39    #[error("requested count too large: {len}")]
40    PayloadRequestTooLarge {
41        /// The length that was requested.
42        len: u64,
43    },
44    /// Too many requested versioned hashes for blobs request
45    #[error("requested blob count too large: {len}")]
46    BlobRequestTooLarge {
47        /// The length that was requested.
48        len: usize,
49    },
50    /// Thrown if `engine_getPayloadBodiesByRangeV1` contains an invalid range
51    #[error("invalid start ({start}) or count ({count})")]
52    InvalidBodiesRange {
53        /// Start of the range
54        start: u64,
55        /// Requested number of items
56        count: u64,
57    },
58    /// Terminal block hash mismatch during transition configuration exchange.
59    #[error(
60        "invalid transition terminal block hash: \
61         execution: {execution:?}, consensus: {consensus}"
62    )]
63    TerminalBlockHash {
64        /// Execution terminal block hash. `None` if block number is not found in the database.
65        execution: Option<B256>,
66        /// Consensus terminal block hash.
67        consensus: B256,
68    },
69    /// An error occurred while processing the fork choice update in the beacon consensus engine.
70    #[error(transparent)]
71    ForkChoiceUpdate(#[from] BeaconForkChoiceUpdateError),
72    /// An error occurred while processing a new payload in the beacon consensus engine.
73    #[error(transparent)]
74    NewPayload(#[from] BeaconOnNewPayloadError),
75    /// Encountered an internal error.
76    #[error(transparent)]
77    Internal(#[from] Box<dyn core::error::Error + Send + Sync>),
78    /// Fetching the payload failed
79    #[error(transparent)]
80    GetPayloadError(#[from] PayloadBuilderError),
81    /// The payload or attributes are known to be malformed before processing.
82    #[error(transparent)]
83    EngineObjectValidationError(#[from] EngineObjectValidationError),
84    /// Requests hash provided, but can't be accepted by the API.
85    #[error("requests hash cannot be accepted by the API without `--engine.accept-execution-requests-hash` flag")]
86    UnexpectedRequestsHash,
87    /// Any other rpc error
88    #[error("{0}")]
89    Other(jsonrpsee_types::ErrorObject<'static>),
90}
91
92impl EngineApiError {
93    /// Crates a new [`EngineApiError::Other`] variant.
94    pub const fn other(err: jsonrpsee_types::ErrorObject<'static>) -> Self {
95        Self::Other(err)
96    }
97}
98
99/// Helper type to represent the `error` field in the error response:
100/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
101#[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            // Per the Shanghai Engine API spec, FCU V2 must return -38003 when the wrong
117            // PayloadAttributes version is used.
118            // Spec: https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md
119            // Change: https://github.com/ethereum/execution-apis/pull/761
120            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                // Note: the data field is not required by the spec, but is also included by other
133                // clients
134                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            // Error responses from the consensus engine
181            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                    // Map future alloy forkchoice errors as internal until handled.
206                    #[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                // Only attributes that the payload builder rejects are invalid payload attributes.
216                // Any other failure to start the payload job is a failure of this node and must
217                // not be reported as invalid attributes, which the consensus client would discard.
218                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            // Any other server error
236            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    // Tests that engine errors are formatted correctly according to the engine API spec
266    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
267    #[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        // ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes is for semantic validation
292        // errors that occur AFTER the structure check passes, so it returns -38003
293        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        // Processing errors explicitly classified as malformed request parameters are rejected
316        // with an invalid params error instead of an `INVALID` payload status.
317        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        // Per the Shanghai Engine API spec, FCU V2 must return -38003 when the wrong
326        // PayloadAttributes version is used.
327        // Spec: https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md
328        // Change: https://github.com/ethereum/execution-apis/pull/761
329        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        // Beacon root shape mismatches on PayloadAttributes are reported as -38003.
366        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}