reth_rpc_engine_api/
error.rs

1use alloy_primitives::{B256, U256};
2use jsonrpsee_types::error::{
3    INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE, INVALID_PARAMS_MSG, SERVER_ERROR_MSG,
4};
5use reth_engine_primitives::{BeaconForkChoiceUpdateError, BeaconOnNewPayloadError};
6use reth_payload_builder_primitives::PayloadBuilderError;
7use reth_payload_primitives::EngineObjectValidationError;
8use thiserror::Error;
9
10/// The Engine API result type
11pub type EngineApiResult<Ok> = Result<Ok, EngineApiError>;
12
13/// Invalid payload attributes code.
14pub const INVALID_PAYLOAD_ATTRIBUTES: i32 = -38003;
15/// Payload unsupported fork code.
16pub const UNSUPPORTED_FORK_CODE: i32 = -38005;
17/// Payload unknown error code.
18pub const UNKNOWN_PAYLOAD_CODE: i32 = -38001;
19/// Request too large error code.
20pub const REQUEST_TOO_LARGE_CODE: i32 = -38004;
21
22/// Error message for the request too large error.
23const REQUEST_TOO_LARGE_MESSAGE: &str = "Too large request";
24
25/// Error message for the request too large error.
26const INVALID_PAYLOAD_ATTRIBUTES_MSG: &str = "Invalid payload attributes";
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 total difficulty mismatch during transition configuration exchange.
59    #[error(
60        "invalid transition terminal total difficulty: \
61         execution: {execution}, consensus: {consensus}"
62    )]
63    TerminalTD {
64        /// Execution terminal total difficulty value.
65        execution: U256,
66        /// Consensus terminal total difficulty value.
67        consensus: U256,
68    },
69    /// Terminal block hash mismatch during transition configuration exchange.
70    #[error(
71        "invalid transition terminal block hash: \
72         execution: {execution:?}, consensus: {consensus}"
73    )]
74    TerminalBlockHash {
75        /// Execution terminal block hash. `None` if block number is not found in the database.
76        execution: Option<B256>,
77        /// Consensus terminal block hash.
78        consensus: B256,
79    },
80    /// An error occurred while processing the fork choice update in the beacon consensus engine.
81    #[error(transparent)]
82    ForkChoiceUpdate(#[from] BeaconForkChoiceUpdateError),
83    /// An error occurred while processing a new payload in the beacon consensus engine.
84    #[error(transparent)]
85    NewPayload(#[from] BeaconOnNewPayloadError),
86    /// Encountered an internal error.
87    #[error(transparent)]
88    Internal(#[from] Box<dyn core::error::Error + Send + Sync>),
89    /// Fetching the payload failed
90    #[error(transparent)]
91    GetPayloadError(#[from] PayloadBuilderError),
92    /// The payload or attributes are known to be malformed before processing.
93    #[error(transparent)]
94    EngineObjectValidationError(#[from] EngineObjectValidationError),
95    /// Requests hash provided, but can't be accepted by the API.
96    #[error("requests hash cannot be accepted by the API without `--engine.accept-execution-requests-hash` flag")]
97    UnexpectedRequestsHash,
98    /// Any other rpc error
99    #[error("{0}")]
100    Other(jsonrpsee_types::ErrorObject<'static>),
101}
102
103impl EngineApiError {
104    /// Crates a new [`EngineApiError::Other`] variant.
105    pub const fn other(err: jsonrpsee_types::ErrorObject<'static>) -> Self {
106        Self::Other(err)
107    }
108}
109
110/// Helper type to represent the `error` field in the error response:
111/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
112#[derive(serde::Serialize)]
113struct ErrorData {
114    err: String,
115}
116
117impl ErrorData {
118    #[inline]
119    fn new(err: impl std::fmt::Display) -> Self {
120        Self { err: err.to_string() }
121    }
122}
123
124impl From<EngineApiError> for jsonrpsee_types::error::ErrorObject<'static> {
125    fn from(error: EngineApiError) -> Self {
126        match error {
127            EngineApiError::InvalidBodiesRange { .. } |
128            EngineApiError::EngineObjectValidationError(
129                EngineObjectValidationError::Payload(_) |
130                EngineObjectValidationError::InvalidParams(_),
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_PARAMS_CODE,
137                    INVALID_PARAMS_MSG,
138                    Some(ErrorData::new(error)),
139                )
140            }
141            EngineApiError::EngineObjectValidationError(
142                EngineObjectValidationError::PayloadAttributes(_),
143            ) => {
144                // Note: the data field is not required by the spec, but is also included by other
145                // clients
146                jsonrpsee_types::error::ErrorObject::owned(
147                    INVALID_PAYLOAD_ATTRIBUTES,
148                    INVALID_PAYLOAD_ATTRIBUTES_MSG,
149                    Some(ErrorData::new(error)),
150                )
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::UnsupportedFork,
167            ) => jsonrpsee_types::error::ErrorObject::owned(
168                UNSUPPORTED_FORK_CODE,
169                error.to_string(),
170                None::<()>,
171            ),
172            // Error responses from the consensus engine
173            EngineApiError::ForkChoiceUpdate(ref err) => match err {
174                BeaconForkChoiceUpdateError::ForkchoiceUpdateError(err) => (*err).into(),
175                BeaconForkChoiceUpdateError::EngineUnavailable |
176                BeaconForkChoiceUpdateError::Internal(_) => {
177                    jsonrpsee_types::error::ErrorObject::owned(
178                        INTERNAL_ERROR_CODE,
179                        SERVER_ERROR_MSG,
180                        Some(ErrorData::new(error)),
181                    )
182                }
183            },
184            // Any other server error
185            EngineApiError::TerminalTD { .. } |
186            EngineApiError::TerminalBlockHash { .. } |
187            EngineApiError::NewPayload(_) |
188            EngineApiError::Internal(_) |
189            EngineApiError::GetPayloadError(_) => jsonrpsee_types::error::ErrorObject::owned(
190                INTERNAL_ERROR_CODE,
191                SERVER_ERROR_MSG,
192                Some(ErrorData::new(error)),
193            ),
194            EngineApiError::Other(err) => err,
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use alloy_rpc_types_engine::ForkchoiceUpdateError;
203
204    #[track_caller]
205    fn ensure_engine_rpc_error(
206        code: i32,
207        message: &str,
208        err: impl Into<jsonrpsee_types::error::ErrorObject<'static>>,
209    ) {
210        let err = err.into();
211        assert_eq!(err.code(), code);
212        assert_eq!(err.message(), message);
213    }
214
215    // Tests that engine errors are formatted correctly according to the engine API spec
216    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
217    #[test]
218    fn engine_error_rpc_error_test() {
219        ensure_engine_rpc_error(
220            UNSUPPORTED_FORK_CODE,
221            "Unsupported fork",
222            EngineApiError::EngineObjectValidationError(
223                EngineObjectValidationError::UnsupportedFork,
224            ),
225        );
226
227        ensure_engine_rpc_error(
228            REQUEST_TOO_LARGE_CODE,
229            "Too large request",
230            EngineApiError::PayloadRequestTooLarge { len: 0 },
231        );
232
233        ensure_engine_rpc_error(
234            -38002,
235            "Invalid forkchoice state",
236            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
237                ForkchoiceUpdateError::InvalidState,
238            )),
239        );
240
241        ensure_engine_rpc_error(
242            -38003,
243            "Invalid payload attributes",
244            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
245                ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
246            )),
247        );
248
249        ensure_engine_rpc_error(
250            UNKNOWN_PAYLOAD_CODE,
251            "Unknown payload",
252            EngineApiError::UnknownPayload,
253        );
254    }
255}