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,
5};
6use jsonrpsee_types::error::{
7    INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE, INVALID_PARAMS_MSG, SERVER_ERROR_MSG,
8};
9use reth_engine_primitives::{BeaconForkChoiceUpdateError, BeaconOnNewPayloadError};
10use reth_payload_builder_primitives::PayloadBuilderError;
11use reth_payload_primitives::{EngineObjectValidationError, VersionSpecificValidationError};
12use thiserror::Error;
13
14/// The Engine API result type
15pub type EngineApiResult<Ok> = Result<Ok, EngineApiError>;
16
17/// Payload unsupported fork code.
18pub const UNSUPPORTED_FORK_CODE: i32 = -38005;
19/// Payload unknown error code.
20pub const UNKNOWN_PAYLOAD_CODE: i32 = -38001;
21/// Request too large error code.
22pub const REQUEST_TOO_LARGE_CODE: i32 = -38004;
23
24/// Error message for the request too large error.
25const REQUEST_TOO_LARGE_MESSAGE: &str = "Too large request";
26
27/// Error returned by [`EngineApi`][crate::EngineApi]
28///
29/// Note: This is a high-fidelity error type which can be converted to an RPC error that adheres to
30/// the [Engine API spec](https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors).
31#[derive(Error, Debug)]
32pub enum EngineApiError {
33    // **IMPORTANT**: keep error messages in sync with the Engine API spec linked above.
34    /// Payload does not exist / is not available.
35    #[error("Unknown payload")]
36    UnknownPayload,
37    /// The payload body request length is too large.
38    #[error("requested count too large: {len}")]
39    PayloadRequestTooLarge {
40        /// The length that was requested.
41        len: u64,
42    },
43    /// Too many requested versioned hashes for blobs request
44    #[error("requested blob count too large: {len}")]
45    BlobRequestTooLarge {
46        /// The length that was requested.
47        len: usize,
48    },
49    /// Thrown if `engine_getPayloadBodiesByRangeV1` contains an invalid range
50    #[error("invalid start ({start}) or count ({count})")]
51    InvalidBodiesRange {
52        /// Start of the range
53        start: u64,
54        /// Requested number of items
55        count: u64,
56    },
57    /// Terminal block hash mismatch during transition configuration exchange.
58    #[error(
59        "invalid transition terminal block hash: \
60         execution: {execution:?}, consensus: {consensus}"
61    )]
62    TerminalBlockHash {
63        /// Execution terminal block hash. `None` if block number is not found in the database.
64        execution: Option<B256>,
65        /// Consensus terminal block hash.
66        consensus: B256,
67    },
68    /// An error occurred while processing the fork choice update in the beacon consensus engine.
69    #[error(transparent)]
70    ForkChoiceUpdate(#[from] BeaconForkChoiceUpdateError),
71    /// An error occurred while processing a new payload in the beacon consensus engine.
72    #[error(transparent)]
73    NewPayload(#[from] BeaconOnNewPayloadError),
74    /// Encountered an internal error.
75    #[error(transparent)]
76    Internal(#[from] Box<dyn core::error::Error + Send + Sync>),
77    /// Fetching the payload failed
78    #[error(transparent)]
79    GetPayloadError(#[from] PayloadBuilderError),
80    /// The payload or attributes are known to be malformed before processing.
81    #[error(transparent)]
82    EngineObjectValidationError(#[from] EngineObjectValidationError),
83    /// Requests hash provided, but can't be accepted by the API.
84    #[error("requests hash cannot be accepted by the API without `--engine.accept-execution-requests-hash` flag")]
85    UnexpectedRequestsHash,
86    /// Any other rpc error
87    #[error("{0}")]
88    Other(jsonrpsee_types::ErrorObject<'static>),
89}
90
91impl EngineApiError {
92    /// Crates a new [`EngineApiError::Other`] variant.
93    pub const fn other(err: jsonrpsee_types::ErrorObject<'static>) -> Self {
94        Self::Other(err)
95    }
96}
97
98/// Helper type to represent the `error` field in the error response:
99/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
100#[derive(serde::Serialize)]
101struct ErrorData {
102    err: String,
103}
104
105impl ErrorData {
106    #[inline]
107    fn new(err: impl std::fmt::Display) -> Self {
108        Self { err: err.to_string() }
109    }
110}
111
112impl From<EngineApiError> for jsonrpsee_types::error::ErrorObject<'static> {
113    fn from(error: EngineApiError) -> Self {
114        match error {
115            // Per the Shanghai Engine API spec, FCU V2 must return -38003 when the wrong
116            // PayloadAttributes version is used.
117            // Spec: https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md
118            // Change: https://github.com/ethereum/execution-apis/pull/761
119            EngineApiError::EngineObjectValidationError(
120                EngineObjectValidationError::PayloadAttributes(
121                    VersionSpecificValidationError::WithdrawalsNotSupportedInV1 |
122                    VersionSpecificValidationError::NoWithdrawalsPostShanghai |
123                    VersionSpecificValidationError::HasWithdrawalsPreShanghai |
124                    VersionSpecificValidationError::BlockAccessListNotSupported |
125                    VersionSpecificValidationError::HasBlockAccessListPreAmsterdam |
126                    VersionSpecificValidationError::NoBlockAccessListPostAmsterdam |
127                    VersionSpecificValidationError::HasSlotNumberPreAmsterdam |
128                    VersionSpecificValidationError::NoSlotNumberPostAmsterdam |
129                    VersionSpecificValidationError::SlotNumberNotSupported,
130                ),
131            ) |
132            EngineApiError::UnexpectedRequestsHash => {
133                // Note: the data field is not required by the spec, but is also included by other
134                // clients
135                jsonrpsee_types::error::ErrorObject::owned(
136                    INVALID_PAYLOAD_ATTRIBUTES_ERROR,
137                    INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
138                    Some(ErrorData::new(error)),
139                )
140            }
141            EngineApiError::InvalidBodiesRange { .. } |
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                    // Map future alloy forkchoice errors as internal until handled.
199                    #[allow(unreachable_patterns, clippy::needless_return)]
200                    _ => {
201                        return jsonrpsee_types::error::ErrorObject::owned(
202                            INTERNAL_ERROR_CODE,
203                            SERVER_ERROR_MSG,
204                            Some(ErrorData::new(error)),
205                        );
206                    }
207                },
208                BeaconForkChoiceUpdateError::EngineUnavailable |
209                BeaconForkChoiceUpdateError::Internal(_) => {
210                    jsonrpsee_types::error::ErrorObject::owned(
211                        INTERNAL_ERROR_CODE,
212                        SERVER_ERROR_MSG,
213                        Some(ErrorData::new(error)),
214                    )
215                }
216            },
217            // Any other server error
218            EngineApiError::TerminalBlockHash { .. } |
219            EngineApiError::NewPayload(_) |
220            EngineApiError::Internal(_) |
221            EngineApiError::GetPayloadError(_) => jsonrpsee_types::error::ErrorObject::owned(
222                INTERNAL_ERROR_CODE,
223                SERVER_ERROR_MSG,
224                Some(ErrorData::new(error)),
225            ),
226            EngineApiError::Other(err) => err,
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use alloy_rpc_types_engine::ForkchoiceUpdateError;
235    #[track_caller]
236    fn ensure_engine_rpc_error(
237        code: i32,
238        message: &str,
239        err: impl Into<jsonrpsee_types::error::ErrorObject<'static>>,
240    ) {
241        let err = err.into();
242        assert_eq!(err.code(), code);
243        assert_eq!(err.message(), message);
244    }
245
246    // Tests that engine errors are formatted correctly according to the engine API spec
247    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
248    #[test]
249    fn engine_error_rpc_error_test() {
250        ensure_engine_rpc_error(
251            UNSUPPORTED_FORK_CODE,
252            "Unsupported fork",
253            EngineApiError::EngineObjectValidationError(
254                EngineObjectValidationError::UnsupportedFork,
255            ),
256        );
257
258        ensure_engine_rpc_error(
259            REQUEST_TOO_LARGE_CODE,
260            "Too large request",
261            EngineApiError::PayloadRequestTooLarge { len: 0 },
262        );
263
264        ensure_engine_rpc_error(
265            -38002,
266            "Invalid forkchoice state",
267            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
268                ForkchoiceUpdateError::InvalidState,
269            )),
270        );
271
272        // ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes is for semantic validation
273        // errors that occur AFTER the structure check passes, so it returns -38003
274        ensure_engine_rpc_error(
275            -38003,
276            "Invalid payload attributes",
277            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
278                ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
279            )),
280        );
281
282        ensure_engine_rpc_error(
283            UNKNOWN_PAYLOAD_CODE,
284            "Unknown payload",
285            EngineApiError::UnknownPayload,
286        );
287
288        // Per the Shanghai Engine API spec, FCU V2 must return -38003 when the wrong
289        // PayloadAttributes version is used.
290        // Spec: https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md
291        // Change: https://github.com/ethereum/execution-apis/pull/761
292        ensure_engine_rpc_error(
293            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
294            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
295            EngineApiError::EngineObjectValidationError(
296                EngineObjectValidationError::PayloadAttributes(
297                    VersionSpecificValidationError::NoWithdrawalsPostShanghai,
298                ),
299            ),
300        );
301
302        ensure_engine_rpc_error(
303            INVALID_PARAMS_CODE,
304            INVALID_PARAMS_MSG,
305            EngineApiError::EngineObjectValidationError(EngineObjectValidationError::Payload(
306                VersionSpecificValidationError::NoWithdrawalsPostShanghai,
307            )),
308        );
309
310        ensure_engine_rpc_error(
311            INVALID_PARAMS_CODE,
312            INVALID_PARAMS_MSG,
313            EngineApiError::EngineObjectValidationError(EngineObjectValidationError::Payload(
314                VersionSpecificValidationError::HasWithdrawalsPreShanghai,
315            )),
316        );
317
318        ensure_engine_rpc_error(
319            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
320            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
321            EngineApiError::EngineObjectValidationError(
322                EngineObjectValidationError::PayloadAttributes(
323                    VersionSpecificValidationError::HasWithdrawalsPreShanghai,
324                ),
325            ),
326        );
327
328        // Beacon root shape mismatches on PayloadAttributes are reported as -38003.
329        ensure_engine_rpc_error(
330            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
331            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
332            EngineApiError::EngineObjectValidationError(
333                EngineObjectValidationError::PayloadAttributes(
334                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
335                ),
336            ),
337        );
338    }
339}