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::HasBlockAccessListPreAmsterdam |
127                    VersionSpecificValidationError::NoBlockAccessListPostAmsterdam |
128                    VersionSpecificValidationError::HasSlotNumberPreAmsterdam |
129                    VersionSpecificValidationError::NoSlotNumberPostAmsterdam |
130                    VersionSpecificValidationError::SlotNumberNotSupported,
131                ),
132            ) |
133            EngineApiError::UnexpectedRequestsHash => {
134                // Note: the data field is not required by the spec, but is also included by other
135                // clients
136                jsonrpsee_types::error::ErrorObject::owned(
137                    INVALID_PAYLOAD_ATTRIBUTES_ERROR,
138                    INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
139                    Some(ErrorData::new(error)),
140                )
141            }
142            EngineApiError::InvalidBodiesRange { .. } |
143            EngineApiError::NewPayload(BeaconOnNewPayloadError::InvalidParams(_)) |
144            EngineApiError::EngineObjectValidationError(
145                EngineObjectValidationError::Payload(_) |
146                EngineObjectValidationError::InvalidParams(_),
147            ) => jsonrpsee_types::error::ErrorObject::owned(
148                INVALID_PARAMS_CODE,
149                INVALID_PARAMS_MSG,
150                Some(ErrorData::new(error)),
151            ),
152            EngineApiError::UnknownPayload => jsonrpsee_types::error::ErrorObject::owned(
153                UNKNOWN_PAYLOAD_CODE,
154                error.to_string(),
155                None::<()>,
156            ),
157            EngineApiError::PayloadRequestTooLarge { .. } |
158            EngineApiError::BlobRequestTooLarge { .. } => {
159                jsonrpsee_types::error::ErrorObject::owned(
160                    REQUEST_TOO_LARGE_CODE,
161                    REQUEST_TOO_LARGE_MESSAGE,
162                    Some(ErrorData::new(error)),
163                )
164            }
165            EngineApiError::EngineObjectValidationError(
166                EngineObjectValidationError::PayloadAttributes(
167                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3 |
168                    VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun,
169                ),
170            ) => jsonrpsee_types::error::ErrorObject::owned(
171                INVALID_PAYLOAD_ATTRIBUTES_ERROR,
172                INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
173                Some(ErrorData::new(error)),
174            ),
175            EngineApiError::EngineObjectValidationError(
176                EngineObjectValidationError::UnsupportedFork,
177            ) => jsonrpsee_types::error::ErrorObject::owned(
178                UNSUPPORTED_FORK_CODE,
179                error.to_string(),
180                None::<()>,
181            ),
182            // Error responses from the consensus engine
183            EngineApiError::ForkChoiceUpdate(ref err) => match err {
184                BeaconForkChoiceUpdateError::ForkchoiceUpdateError(err) => match err {
185                    ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes => {
186                        jsonrpsee_types::error::ErrorObject::owned(
187                            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
188                            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
189                            None::<()>,
190                        )
191                    }
192                    ForkchoiceUpdateError::InvalidState |
193                    ForkchoiceUpdateError::UnknownFinalBlock => {
194                        jsonrpsee_types::error::ErrorObject::owned(
195                            INVALID_FORK_CHOICE_STATE_ERROR,
196                            INVALID_FORK_CHOICE_STATE_ERROR_MSG,
197                            None::<()>,
198                        )
199                    }
200                    ForkchoiceUpdateError::TooDeepReorg => {
201                        jsonrpsee_types::error::ErrorObject::owned(
202                            TOO_DEEP_REORG_ERROR,
203                            TOO_DEEP_REORG_ERROR_MSG,
204                            None::<()>,
205                        )
206                    }
207                    // Map future alloy forkchoice errors as internal until handled.
208                    #[allow(unreachable_patterns, clippy::needless_return)]
209                    _ => {
210                        return jsonrpsee_types::error::ErrorObject::owned(
211                            INTERNAL_ERROR_CODE,
212                            SERVER_ERROR_MSG,
213                            Some(ErrorData::new(error)),
214                        );
215                    }
216                },
217                // Only attributes that the payload builder rejects are invalid payload attributes.
218                // Any other failure to start the payload job is a failure of this node and must
219                // not be reported as invalid attributes, which the consensus client would discard.
220                BeaconForkChoiceUpdateError::PayloadJob(
221                    PayloadBuilderError::InvalidAttributes(_),
222                ) => jsonrpsee_types::error::ErrorObject::owned(
223                    INVALID_PAYLOAD_ATTRIBUTES_ERROR,
224                    INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
225                    Some(ErrorData::new(error)),
226                ),
227                BeaconForkChoiceUpdateError::EngineUnavailable |
228                BeaconForkChoiceUpdateError::PayloadJob(_) |
229                BeaconForkChoiceUpdateError::Internal(_) => {
230                    jsonrpsee_types::error::ErrorObject::owned(
231                        INTERNAL_ERROR_CODE,
232                        SERVER_ERROR_MSG,
233                        Some(ErrorData::new(error)),
234                    )
235                }
236            },
237            // Any other server error
238            EngineApiError::TerminalBlockHash { .. } |
239            EngineApiError::NewPayload(_) |
240            EngineApiError::Internal(_) |
241            EngineApiError::GetPayloadError(_) => jsonrpsee_types::error::ErrorObject::owned(
242                INTERNAL_ERROR_CODE,
243                SERVER_ERROR_MSG,
244                Some(ErrorData::new(error)),
245            ),
246            EngineApiError::Other(err) => err,
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use alloy_rpc_types_engine::ForkchoiceUpdateError;
255    use reth_payload_primitives::InvalidPayloadAttributesError;
256    #[track_caller]
257    fn ensure_engine_rpc_error(
258        code: i32,
259        message: &str,
260        err: impl Into<jsonrpsee_types::error::ErrorObject<'static>>,
261    ) {
262        let err = err.into();
263        assert_eq!(err.code(), code);
264        assert_eq!(err.message(), message);
265    }
266
267    // Tests that engine errors are formatted correctly according to the engine API spec
268    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
269    #[test]
270    fn engine_error_rpc_error_test() {
271        ensure_engine_rpc_error(
272            UNSUPPORTED_FORK_CODE,
273            "Unsupported fork",
274            EngineApiError::EngineObjectValidationError(
275                EngineObjectValidationError::UnsupportedFork,
276            ),
277        );
278
279        ensure_engine_rpc_error(
280            REQUEST_TOO_LARGE_CODE,
281            "Too large request",
282            EngineApiError::PayloadRequestTooLarge { len: 0 },
283        );
284
285        ensure_engine_rpc_error(
286            -38002,
287            "Invalid forkchoice state",
288            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
289                ForkchoiceUpdateError::InvalidState,
290            )),
291        );
292
293        // ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes is for semantic validation
294        // errors that occur AFTER the structure check passes, so it returns -38003
295        ensure_engine_rpc_error(
296            -38003,
297            "Invalid payload attributes",
298            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
299                ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
300            )),
301        );
302
303        ensure_engine_rpc_error(
304            -38006,
305            "Too deep reorg",
306            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
307                ForkchoiceUpdateError::TooDeepReorg,
308            )),
309        );
310
311        ensure_engine_rpc_error(
312            UNKNOWN_PAYLOAD_CODE,
313            "Unknown payload",
314            EngineApiError::UnknownPayload,
315        );
316
317        // Processing errors explicitly classified as malformed request parameters are rejected
318        // with an invalid params error instead of an `INVALID` payload status.
319        ensure_engine_rpc_error(
320            INVALID_PARAMS_CODE,
321            INVALID_PARAMS_MSG,
322            EngineApiError::NewPayload(BeaconOnNewPayloadError::InvalidParams(
323                "malformed payload parameters".into(),
324            )),
325        );
326
327        // Per the Shanghai Engine API spec, FCU V2 must return -38003 when the wrong
328        // PayloadAttributes version is used.
329        // Spec: https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md
330        // Change: https://github.com/ethereum/execution-apis/pull/761
331        ensure_engine_rpc_error(
332            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
333            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
334            EngineApiError::EngineObjectValidationError(
335                EngineObjectValidationError::PayloadAttributes(
336                    VersionSpecificValidationError::NoWithdrawalsPostShanghai,
337                ),
338            ),
339        );
340
341        ensure_engine_rpc_error(
342            INVALID_PARAMS_CODE,
343            INVALID_PARAMS_MSG,
344            EngineApiError::EngineObjectValidationError(EngineObjectValidationError::Payload(
345                VersionSpecificValidationError::NoWithdrawalsPostShanghai,
346            )),
347        );
348
349        ensure_engine_rpc_error(
350            INVALID_PARAMS_CODE,
351            INVALID_PARAMS_MSG,
352            EngineApiError::EngineObjectValidationError(EngineObjectValidationError::Payload(
353                VersionSpecificValidationError::HasWithdrawalsPreShanghai,
354            )),
355        );
356
357        ensure_engine_rpc_error(
358            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
359            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
360            EngineApiError::EngineObjectValidationError(
361                EngineObjectValidationError::PayloadAttributes(
362                    VersionSpecificValidationError::HasWithdrawalsPreShanghai,
363                ),
364            ),
365        );
366
367        // Beacon root shape mismatches on PayloadAttributes are reported as -38003.
368        ensure_engine_rpc_error(
369            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
370            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
371            EngineApiError::EngineObjectValidationError(
372                EngineObjectValidationError::PayloadAttributes(
373                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
374                ),
375            ),
376        );
377    }
378
379    fn payload_job_error(err: PayloadBuilderError) -> jsonrpsee_types::error::ErrorObject<'static> {
380        EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::PayloadJob(err)).into()
381    }
382
383    #[test]
384    fn payload_job_rejecting_the_attributes_is_invalid_payload_attributes() {
385        let err = payload_job_error(PayloadBuilderError::invalid_attributes(
386            InvalidPayloadAttributesError::InvalidTimestamp,
387        ));
388
389        assert_eq!(err.code(), INVALID_PAYLOAD_ATTRIBUTES_ERROR);
390        assert_eq!(err.message(), INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG);
391        assert_eq!(
392            err.data().unwrap().get(),
393            r#"{"err":"failed to start payload job: invalid payload attributes: invalid timestamp"}"#
394        );
395    }
396
397    #[test]
398    fn payload_job_failing_on_this_node_is_an_internal_error() {
399        let node_local = [
400            PayloadBuilderError::MissingParentHeader(B256::ZERO),
401            PayloadBuilderError::ChannelClosed,
402            PayloadBuilderError::other(std::io::Error::other("other")),
403        ];
404
405        for payload_job in node_local {
406            let message = payload_job.to_string();
407            let err = payload_job_error(payload_job);
408            assert_eq!(err.code(), INTERNAL_ERROR_CODE, "{message}");
409            assert_eq!(err.message(), SERVER_ERROR_MSG, "{message}");
410            assert_eq!(
411                err.data().unwrap().get(),
412                format!(r#"{{"err":"failed to start payload job: {message}"}}"#)
413            );
414        }
415    }
416}