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            EngineApiError::InvalidBodiesRange { .. } |
116            EngineApiError::EngineObjectValidationError(
117                EngineObjectValidationError::Payload(_) |
118                EngineObjectValidationError::InvalidParams(_) |
119                // Per Engine API spec, structure validation errors for PayloadAttributes
120                // (e.g., missing withdrawals post-Shanghai) should return -32602 "Invalid params".
121                // See: https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md
122                // Fixes: https://github.com/paradigmxyz/reth/issues/8732
123                EngineObjectValidationError::PayloadAttributes(
124                    VersionSpecificValidationError::WithdrawalsNotSupportedInV1 |
125                    VersionSpecificValidationError::NoWithdrawalsPostShanghai |
126                    VersionSpecificValidationError::HasWithdrawalsPreShanghai,
127                ),
128            ) |
129            EngineApiError::UnexpectedRequestsHash => {
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::UnknownPayload => jsonrpsee_types::error::ErrorObject::owned(
139                UNKNOWN_PAYLOAD_CODE,
140                error.to_string(),
141                None::<()>,
142            ),
143            EngineApiError::PayloadRequestTooLarge { .. } |
144            EngineApiError::BlobRequestTooLarge { .. } => {
145                jsonrpsee_types::error::ErrorObject::owned(
146                    REQUEST_TOO_LARGE_CODE,
147                    REQUEST_TOO_LARGE_MESSAGE,
148                    Some(ErrorData::new(error)),
149                )
150            }
151            EngineApiError::EngineObjectValidationError(
152                EngineObjectValidationError::PayloadAttributes(
153                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3 |
154                    VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun,
155                ),
156            ) => jsonrpsee_types::error::ErrorObject::owned(
157                INVALID_PAYLOAD_ATTRIBUTES_ERROR,
158                INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
159                Some(ErrorData::new(error)),
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) => match err {
171                    ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes => {
172                        jsonrpsee_types::error::ErrorObject::owned(
173                            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
174                            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
175                            None::<()>,
176                        )
177                    }
178                    ForkchoiceUpdateError::InvalidState |
179                    ForkchoiceUpdateError::UnknownFinalBlock => {
180                        jsonrpsee_types::error::ErrorObject::owned(
181                            INVALID_FORK_CHOICE_STATE_ERROR,
182                            INVALID_FORK_CHOICE_STATE_ERROR_MSG,
183                            None::<()>,
184                        )
185                    }
186                },
187                BeaconForkChoiceUpdateError::EngineUnavailable |
188                BeaconForkChoiceUpdateError::Internal(_) => {
189                    jsonrpsee_types::error::ErrorObject::owned(
190                        INTERNAL_ERROR_CODE,
191                        SERVER_ERROR_MSG,
192                        Some(ErrorData::new(error)),
193                    )
194                }
195            },
196            // Any other server error
197            EngineApiError::TerminalBlockHash { .. } |
198            EngineApiError::NewPayload(_) |
199            EngineApiError::Internal(_) |
200            EngineApiError::GetPayloadError(_) => jsonrpsee_types::error::ErrorObject::owned(
201                INTERNAL_ERROR_CODE,
202                SERVER_ERROR_MSG,
203                Some(ErrorData::new(error)),
204            ),
205            EngineApiError::Other(err) => err,
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use alloy_rpc_types_engine::ForkchoiceUpdateError;
214    #[track_caller]
215    fn ensure_engine_rpc_error(
216        code: i32,
217        message: &str,
218        err: impl Into<jsonrpsee_types::error::ErrorObject<'static>>,
219    ) {
220        let err = err.into();
221        assert_eq!(err.code(), code);
222        assert_eq!(err.message(), message);
223    }
224
225    // Tests that engine errors are formatted correctly according to the engine API spec
226    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
227    #[test]
228    fn engine_error_rpc_error_test() {
229        ensure_engine_rpc_error(
230            UNSUPPORTED_FORK_CODE,
231            "Unsupported fork",
232            EngineApiError::EngineObjectValidationError(
233                EngineObjectValidationError::UnsupportedFork,
234            ),
235        );
236
237        ensure_engine_rpc_error(
238            REQUEST_TOO_LARGE_CODE,
239            "Too large request",
240            EngineApiError::PayloadRequestTooLarge { len: 0 },
241        );
242
243        ensure_engine_rpc_error(
244            -38002,
245            "Invalid forkchoice state",
246            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
247                ForkchoiceUpdateError::InvalidState,
248            )),
249        );
250
251        // ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes is for semantic validation
252        // errors that occur AFTER the structure check passes, so it returns -38003
253        ensure_engine_rpc_error(
254            -38003,
255            "Invalid payload attributes",
256            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
257                ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
258            )),
259        );
260
261        ensure_engine_rpc_error(
262            UNKNOWN_PAYLOAD_CODE,
263            "Unknown payload",
264            EngineApiError::UnknownPayload,
265        );
266
267        // PayloadAttributes structure validation errors (e.g., missing withdrawals post-Shanghai)
268        // should return -32602 per the Engine API spec
269        // See: https://github.com/paradigmxyz/reth/issues/8732
270        ensure_engine_rpc_error(
271            INVALID_PARAMS_CODE,
272            INVALID_PARAMS_MSG,
273            EngineApiError::EngineObjectValidationError(
274                EngineObjectValidationError::PayloadAttributes(
275                    VersionSpecificValidationError::NoWithdrawalsPostShanghai,
276                ),
277            ),
278        );
279
280        // Beacon root shape mismatches on PayloadAttributes are reported as -38003.
281        ensure_engine_rpc_error(
282            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
283            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
284            EngineApiError::EngineObjectValidationError(
285                EngineObjectValidationError::PayloadAttributes(
286                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
287                ),
288            ),
289        );
290    }
291}