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    /// Any other rpc error
96    #[error("{0}")]
97    Other(jsonrpsee_types::ErrorObject<'static>),
98}
99
100impl EngineApiError {
101    /// Crates a new [`EngineApiError::Other`] variant.
102    pub const fn other(err: jsonrpsee_types::ErrorObject<'static>) -> Self {
103        Self::Other(err)
104    }
105}
106
107/// Helper type to represent the `error` field in the error response:
108/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
109#[derive(serde::Serialize)]
110struct ErrorData {
111    err: String,
112}
113
114impl ErrorData {
115    #[inline]
116    fn new(err: impl std::fmt::Display) -> Self {
117        Self { err: err.to_string() }
118    }
119}
120
121impl From<EngineApiError> for jsonrpsee_types::error::ErrorObject<'static> {
122    fn from(error: EngineApiError) -> Self {
123        match error {
124            EngineApiError::InvalidBodiesRange { .. } |
125            EngineApiError::EngineObjectValidationError(
126                EngineObjectValidationError::Payload(_) |
127                EngineObjectValidationError::InvalidParams(_),
128            ) => {
129                // Note: the data field is not required by the spec, but is also included by other
130                // clients
131                jsonrpsee_types::error::ErrorObject::owned(
132                    INVALID_PARAMS_CODE,
133                    INVALID_PARAMS_MSG,
134                    Some(ErrorData::new(error)),
135                )
136            }
137            EngineApiError::EngineObjectValidationError(
138                EngineObjectValidationError::PayloadAttributes(_),
139            ) => {
140                // Note: the data field is not required by the spec, but is also included by other
141                // clients
142                jsonrpsee_types::error::ErrorObject::owned(
143                    INVALID_PAYLOAD_ATTRIBUTES,
144                    INVALID_PAYLOAD_ATTRIBUTES_MSG,
145                    Some(ErrorData::new(error)),
146                )
147            }
148            EngineApiError::UnknownPayload => jsonrpsee_types::error::ErrorObject::owned(
149                UNKNOWN_PAYLOAD_CODE,
150                error.to_string(),
151                None::<()>,
152            ),
153            EngineApiError::PayloadRequestTooLarge { .. } |
154            EngineApiError::BlobRequestTooLarge { .. } => {
155                jsonrpsee_types::error::ErrorObject::owned(
156                    REQUEST_TOO_LARGE_CODE,
157                    REQUEST_TOO_LARGE_MESSAGE,
158                    Some(ErrorData::new(error)),
159                )
160            }
161            EngineApiError::EngineObjectValidationError(
162                EngineObjectValidationError::UnsupportedFork,
163            ) => jsonrpsee_types::error::ErrorObject::owned(
164                UNSUPPORTED_FORK_CODE,
165                error.to_string(),
166                None::<()>,
167            ),
168            // Error responses from the consensus engine
169            EngineApiError::ForkChoiceUpdate(ref err) => match err {
170                BeaconForkChoiceUpdateError::ForkchoiceUpdateError(err) => (*err).into(),
171                BeaconForkChoiceUpdateError::EngineUnavailable |
172                BeaconForkChoiceUpdateError::Internal(_) => {
173                    jsonrpsee_types::error::ErrorObject::owned(
174                        INTERNAL_ERROR_CODE,
175                        SERVER_ERROR_MSG,
176                        Some(ErrorData::new(error)),
177                    )
178                }
179            },
180            // Any other server error
181            EngineApiError::TerminalTD { .. } |
182            EngineApiError::TerminalBlockHash { .. } |
183            EngineApiError::NewPayload(_) |
184            EngineApiError::Internal(_) |
185            EngineApiError::GetPayloadError(_) => jsonrpsee_types::error::ErrorObject::owned(
186                INTERNAL_ERROR_CODE,
187                SERVER_ERROR_MSG,
188                Some(ErrorData::new(error)),
189            ),
190            EngineApiError::Other(err) => err,
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use alloy_rpc_types_engine::ForkchoiceUpdateError;
199
200    #[track_caller]
201    fn ensure_engine_rpc_error(
202        code: i32,
203        message: &str,
204        err: impl Into<jsonrpsee_types::error::ErrorObject<'static>>,
205    ) {
206        let err = err.into();
207        assert_eq!(err.code(), code);
208        assert_eq!(err.message(), message);
209    }
210
211    // Tests that engine errors are formatted correctly according to the engine API spec
212    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
213    #[test]
214    fn engine_error_rpc_error_test() {
215        ensure_engine_rpc_error(
216            UNSUPPORTED_FORK_CODE,
217            "Unsupported fork",
218            EngineApiError::EngineObjectValidationError(
219                EngineObjectValidationError::UnsupportedFork,
220            ),
221        );
222
223        ensure_engine_rpc_error(
224            REQUEST_TOO_LARGE_CODE,
225            "Too large request",
226            EngineApiError::PayloadRequestTooLarge { len: 0 },
227        );
228
229        ensure_engine_rpc_error(
230            -38002,
231            "Invalid forkchoice state",
232            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
233                ForkchoiceUpdateError::InvalidState,
234            )),
235        );
236
237        ensure_engine_rpc_error(
238            -38003,
239            "Invalid payload attributes",
240            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
241                ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
242            )),
243        );
244
245        ensure_engine_rpc_error(
246            UNKNOWN_PAYLOAD_CODE,
247            "Unknown payload",
248            EngineApiError::UnknownPayload,
249        );
250    }
251}