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_beacon_consensus::BeaconForkChoiceUpdateError;
6use reth_engine_primitives::BeaconOnNewPayloadError;
7use reth_payload_builder_primitives::PayloadBuilderError;
8use reth_payload_primitives::EngineObjectValidationError;
9use thiserror::Error;
10
11/// The Engine API result type
12pub type EngineApiResult<Ok> = Result<Ok, EngineApiError>;
13
14/// Invalid payload attributes code.
15pub const INVALID_PAYLOAD_ATTRIBUTES: i32 = -38003;
16/// Payload unsupported fork code.
17pub const UNSUPPORTED_FORK_CODE: i32 = -38005;
18/// Payload unknown error code.
19pub const UNKNOWN_PAYLOAD_CODE: i32 = -38001;
20/// Request too large error code.
21pub const REQUEST_TOO_LARGE_CODE: i32 = -38004;
22
23/// Error message for the request too large error.
24const REQUEST_TOO_LARGE_MESSAGE: &str = "Too large request";
25
26/// Error message for the request too large error.
27const INVALID_PAYLOAD_ATTRIBUTES_MSG: &str = "Invalid payload attributes";
28
29/// Error returned by [`EngineApi`][crate::EngineApi]
30///
31/// Note: This is a high-fidelity error type which can be converted to an RPC error that adheres to
32/// the [Engine API spec](https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors).
33#[derive(Error, Debug)]
34pub enum EngineApiError {
35    // **IMPORTANT**: keep error messages in sync with the Engine API spec linked above.
36    /// Payload does not exist / is not available.
37    #[error("Unknown payload")]
38    UnknownPayload,
39    /// The payload body request length is too large.
40    #[error("requested count too large: {len}")]
41    PayloadRequestTooLarge {
42        /// The length that was requested.
43        len: u64,
44    },
45    /// Too many requested versioned hashes for blobs request
46    #[error("requested blob count too large: {len}")]
47    BlobRequestTooLarge {
48        /// The length that was requested.
49        len: usize,
50    },
51    /// Thrown if `engine_getPayloadBodiesByRangeV1` contains an invalid range
52    #[error("invalid start ({start}) or count ({count})")]
53    InvalidBodiesRange {
54        /// Start of the range
55        start: u64,
56        /// Requested number of items
57        count: u64,
58    },
59    /// Terminal total difficulty mismatch during transition configuration exchange.
60    #[error(
61        "invalid transition terminal total difficulty: \
62         execution: {execution}, consensus: {consensus}"
63    )]
64    TerminalTD {
65        /// Execution terminal total difficulty value.
66        execution: U256,
67        /// Consensus terminal total difficulty value.
68        consensus: U256,
69    },
70    /// Terminal block hash mismatch during transition configuration exchange.
71    #[error(
72        "invalid transition terminal block hash: \
73         execution: {execution:?}, consensus: {consensus}"
74    )]
75    TerminalBlockHash {
76        /// Execution terminal block hash. `None` if block number is not found in the database.
77        execution: Option<B256>,
78        /// Consensus terminal block hash.
79        consensus: B256,
80    },
81    /// An error occurred while processing the fork choice update in the beacon consensus engine.
82    #[error(transparent)]
83    ForkChoiceUpdate(#[from] BeaconForkChoiceUpdateError),
84    /// An error occurred while processing a new payload in the beacon consensus engine.
85    #[error(transparent)]
86    NewPayload(#[from] BeaconOnNewPayloadError),
87    /// Encountered an internal error.
88    #[error(transparent)]
89    Internal(#[from] Box<dyn core::error::Error + Send + Sync>),
90    /// Fetching the payload failed
91    #[error(transparent)]
92    GetPayloadError(#[from] PayloadBuilderError),
93    /// The payload or attributes are known to be malformed before processing.
94    #[error(transparent)]
95    EngineObjectValidationError(#[from] EngineObjectValidationError),
96    /// Any other rpc error
97    #[error("{0}")]
98    Other(jsonrpsee_types::ErrorObject<'static>),
99}
100
101impl EngineApiError {
102    /// Crates a new [`EngineApiError::Other`] variant.
103    pub const fn other(err: jsonrpsee_types::ErrorObject<'static>) -> Self {
104        Self::Other(err)
105    }
106}
107
108/// Helper type to represent the `error` field in the error response:
109/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
110#[derive(serde::Serialize)]
111struct ErrorData {
112    err: String,
113}
114
115impl ErrorData {
116    #[inline]
117    fn new(err: impl std::fmt::Display) -> Self {
118        Self { err: err.to_string() }
119    }
120}
121
122impl From<EngineApiError> for jsonrpsee_types::error::ErrorObject<'static> {
123    fn from(error: EngineApiError) -> Self {
124        match error {
125            EngineApiError::InvalidBodiesRange { .. } |
126            EngineApiError::EngineObjectValidationError(
127                EngineObjectValidationError::Payload(_) |
128                EngineObjectValidationError::InvalidParams(_),
129            ) => {
130                // Note: the data field is not required by the spec, but is also included by other
131                // clients
132                jsonrpsee_types::error::ErrorObject::owned(
133                    INVALID_PARAMS_CODE,
134                    INVALID_PARAMS_MSG,
135                    Some(ErrorData::new(error)),
136                )
137            }
138            EngineApiError::EngineObjectValidationError(
139                EngineObjectValidationError::PayloadAttributes(_),
140            ) => {
141                // Note: the data field is not required by the spec, but is also included by other
142                // clients
143                jsonrpsee_types::error::ErrorObject::owned(
144                    INVALID_PAYLOAD_ATTRIBUTES,
145                    INVALID_PAYLOAD_ATTRIBUTES_MSG,
146                    Some(ErrorData::new(error)),
147                )
148            }
149            EngineApiError::UnknownPayload => jsonrpsee_types::error::ErrorObject::owned(
150                UNKNOWN_PAYLOAD_CODE,
151                error.to_string(),
152                None::<()>,
153            ),
154            EngineApiError::PayloadRequestTooLarge { .. } |
155            EngineApiError::BlobRequestTooLarge { .. } => {
156                jsonrpsee_types::error::ErrorObject::owned(
157                    REQUEST_TOO_LARGE_CODE,
158                    REQUEST_TOO_LARGE_MESSAGE,
159                    Some(ErrorData::new(error)),
160                )
161            }
162            EngineApiError::EngineObjectValidationError(
163                EngineObjectValidationError::UnsupportedFork,
164            ) => jsonrpsee_types::error::ErrorObject::owned(
165                UNSUPPORTED_FORK_CODE,
166                error.to_string(),
167                None::<()>,
168            ),
169            // Error responses from the consensus engine
170            EngineApiError::ForkChoiceUpdate(ref err) => match err {
171                BeaconForkChoiceUpdateError::ForkchoiceUpdateError(err) => (*err).into(),
172                BeaconForkChoiceUpdateError::EngineUnavailable |
173                BeaconForkChoiceUpdateError::Internal(_) => {
174                    jsonrpsee_types::error::ErrorObject::owned(
175                        INTERNAL_ERROR_CODE,
176                        SERVER_ERROR_MSG,
177                        Some(ErrorData::new(error)),
178                    )
179                }
180            },
181            // Any other server error
182            EngineApiError::TerminalTD { .. } |
183            EngineApiError::TerminalBlockHash { .. } |
184            EngineApiError::NewPayload(_) |
185            EngineApiError::Internal(_) |
186            EngineApiError::GetPayloadError(_) => jsonrpsee_types::error::ErrorObject::owned(
187                INTERNAL_ERROR_CODE,
188                SERVER_ERROR_MSG,
189                Some(ErrorData::new(error)),
190            ),
191            EngineApiError::Other(err) => err,
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use alloy_rpc_types_engine::ForkchoiceUpdateError;
200
201    #[track_caller]
202    fn ensure_engine_rpc_error(
203        code: i32,
204        message: &str,
205        err: impl Into<jsonrpsee_types::error::ErrorObject<'static>>,
206    ) {
207        let err = err.into();
208        assert_eq!(err.code(), code);
209        assert_eq!(err.message(), message);
210    }
211
212    // Tests that engine errors are formatted correctly according to the engine API spec
213    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
214    #[test]
215    fn engine_error_rpc_error_test() {
216        ensure_engine_rpc_error(
217            UNSUPPORTED_FORK_CODE,
218            "Unsupported fork",
219            EngineApiError::EngineObjectValidationError(
220                EngineObjectValidationError::UnsupportedFork,
221            ),
222        );
223
224        ensure_engine_rpc_error(
225            REQUEST_TOO_LARGE_CODE,
226            "Too large request",
227            EngineApiError::PayloadRequestTooLarge { len: 0 },
228        );
229
230        ensure_engine_rpc_error(
231            -38002,
232            "Invalid forkchoice state",
233            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
234                ForkchoiceUpdateError::InvalidState,
235            )),
236        );
237
238        ensure_engine_rpc_error(
239            -38003,
240            "Invalid payload attributes",
241            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
242                ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
243            )),
244        );
245
246        ensure_engine_rpc_error(
247            UNKNOWN_PAYLOAD_CODE,
248            "Unknown payload",
249            EngineApiError::UnknownPayload,
250        );
251    }
252}