Skip to main content

reth_node_ethereum/
engine_ssz_proxy.rs

1//! HTTP SSZ transport proxy for the authenticated Engine API server.
2//!
3//! Implements the [EIP-8178] SSZ Engine API routes under `/engine/v1`.
4//!
5//! [EIP-8178]: https://eips.ethereum.org/EIPS/eip-8178
6
7use crate::engine_ssz_witness::{EngineSszWitness, PayloadStatusWithWitness};
8use alloy_consensus::{Transaction, TxEnvelope};
9use alloy_eips::eip7685::Requests;
10use alloy_primitives::{Bytes, B128, B256};
11use alloy_rpc_types_engine::{
12    ssz_engine_types::{
13        BlobsV1Request, BlobsV1Response, BlobsV2Response, BlobsV3Response, BlobsV4Request,
14        BlobsV4Response, BodiesByHashRequest, BodiesResponse, BuiltPayloadAmsterdam,
15        BuiltPayloadOsaka, BuiltPayloadParis, BuiltPayloadPrague, BuiltPayloadShanghai,
16        ExecutionPayloadBodyAmsterdam, ExecutionPayloadBodyParis, ExecutionPayloadBodyShanghai,
17        ExecutionPayloadEnvelopeAmsterdam, ExecutionPayloadEnvelopeCancun,
18        ExecutionPayloadEnvelopeOsaka, ExecutionPayloadEnvelopeParis,
19        ExecutionPayloadEnvelopePrague, ExecutionPayloadEnvelopeShanghai,
20        ForkchoiceUpdateAmsterdam, ForkchoiceUpdateCancun, ForkchoiceUpdateOsaka,
21        ForkchoiceUpdateParis, ForkchoiceUpdatePrague, ForkchoiceUpdateResponse,
22        ForkchoiceUpdateShanghai, Optional, PayloadStatus as EngineSszPayloadStatus,
23        PayloadStatusKind, MAX_BLOBS_REQUEST, MAX_BODIES_REQUEST, MAX_ERROR_BYTES,
24    },
25    CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadBodyV1,
26    ExecutionPayloadFieldV2, ExecutionPayloadSidecar, ForkchoiceState, PayloadAttributes,
27    PayloadId, PayloadStatusEnum, PraguePayloadFields,
28};
29use futures::future::{BoxFuture, Either};
30use http_body_util::{BodyExt, LengthLimitError, Limited};
31use jsonrpsee::server::{HttpBody, HttpRequest, HttpResponse};
32use reth_chainspec::{EthereumHardfork, EthereumHardforks};
33use reth_engine_primitives::EngineApiValidator;
34use reth_ethereum_engine_primitives::EthEngineTypes;
35use reth_provider::{BalProvider, BlockReader, StateProviderFactory};
36use reth_rpc::EngineApi;
37use reth_rpc_engine_api::EngineApiError;
38use reth_transaction_pool::TransactionPool;
39use ssz::Decode;
40use std::{
41    future::Future,
42    sync::Arc,
43    task::{Context, Poll},
44};
45use tokio::sync::RwLock;
46use tower::{BoxError, Layer, Service};
47
48const OCTET_STREAM: &str = "application/octet-stream";
49const APPLICATION_JSON: &str = "application/json";
50const CONTENT_TYPE: &str = "content-type";
51const CACHE_CONTROL: &str = "cache-control";
52const ETH_EXECUTION_VERSION: &str = "eth-execution-version";
53
54const STATUS_OK: u16 = 200;
55const STATUS_BAD_REQUEST: u16 = 400;
56const STATUS_CONFLICT: u16 = 409;
57const STATUS_UNPROCESSABLE_ENTITY: u16 = 422;
58const STATUS_NOT_FOUND: u16 = 404;
59const STATUS_METHOD_NOT_ALLOWED: u16 = 405;
60const STATUS_PAYLOAD_TOO_LARGE: u16 = 413;
61const STATUS_INTERNAL_SERVER_ERROR: u16 = 500;
62const STATUS_SERVICE_UNAVAILABLE: u16 = 503;
63const STATUS_UNSUPPORTED_MEDIA_TYPE: u16 = 415;
64
65const MAX_BLOB_REQUEST_BYTES: usize = 4 + 16 + MAX_BLOBS_REQUEST * 32;
66const MAX_BODIES_REQUEST_BYTES: usize = 4 + MAX_BODIES_REQUEST * 32;
67const MAX_PAYLOAD_BYTES: usize = 64 * 1024 * 1024;
68const PROBLEM_JSON: &str = "application/problem+json";
69
70type EthEngineApi<Provider, Pool, Validator, ChainSpec> =
71    EngineApi<Provider, EthEngineTypes, Pool, Validator, ChainSpec>;
72/// Shared handle used by [`EngineSszProxyLayer`].
73pub struct EngineSszProxyHandle<Api = ()> {
74    state: Arc<RwLock<EngineSszState<Api>>>,
75}
76
77impl<Api> Clone for EngineSszProxyHandle<Api> {
78    fn clone(&self) -> Self {
79        Self { state: self.state.clone() }
80    }
81}
82
83impl<Api> std::fmt::Debug for EngineSszProxyHandle<Api> {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.debug_struct("EngineSszProxyHandle").finish_non_exhaustive()
86    }
87}
88
89impl<Api> EngineSszProxyHandle<Api> {
90    fn new() -> Self {
91        Self {
92            state: Arc::new(RwLock::new(EngineSszState {
93                engine_api: None,
94                witness_handler: None,
95                witness_enabled: false,
96            })),
97        }
98    }
99
100    fn with_engine_api(engine_api: Api) -> Self {
101        Self {
102            state: Arc::new(RwLock::new(EngineSszState {
103                engine_api: Some(engine_api),
104                witness_handler: None,
105                witness_enabled: false,
106            })),
107        }
108    }
109
110    /// Returns whether both the API and generator support the witness extension.
111    pub async fn witness_enabled(&self) -> bool {
112        self.state.read().await.witness_enabled
113    }
114
115    /// Returns the configured witness generator.
116    pub async fn witness_handler(&self) -> Option<Arc<dyn EngineSszWitness>> {
117        self.state.read().await.witness_handler.clone()
118    }
119}
120
121impl<Api: EngineSszApi> EngineSszProxyHandle<Api> {
122    /// Sets the Engine API implementation used by the proxy.
123    pub async fn set_engine_api(&self, engine_api: Api) {
124        let mut state = self.state.write().await;
125        state.engine_api = Some(engine_api);
126        state.update_witness_support();
127    }
128
129    /// Sets the Engine API implementation during synchronous launch wiring.
130    pub fn set_engine_api_sync(&self, engine_api: Api) {
131        let mut state =
132            self.state.try_write().expect("engine api handle should not be locked during launch");
133        state.engine_api = Some(engine_api);
134        state.update_witness_support();
135    }
136
137    /// Sets the witness generator used by `/payloads/witness`.
138    pub async fn set_witness_handler(&self, witness_handler: Arc<dyn EngineSszWitness>) {
139        let mut state = self.state.write().await;
140        state.witness_handler = Some(witness_handler);
141        state.update_witness_support();
142    }
143
144    /// Sets the witness generator during synchronous launch wiring.
145    pub fn set_witness_handler_sync(&self, witness_handler: Arc<dyn EngineSszWitness>) {
146        let mut state =
147            self.state.try_write().expect("witness handle should not be locked during launch");
148        state.witness_handler = Some(witness_handler);
149        state.update_witness_support();
150    }
151}
152
153impl<Api: Clone> EngineSszProxyHandle<Api> {
154    /// Returns the Engine API implementation used by the proxy.
155    pub async fn engine_api(&self) -> Option<Api> {
156        self.state.read().await.engine_api.clone()
157    }
158}
159
160/// A tower layer that intercepts SSZ Engine API routes under `/engine/v1`.
161#[derive(Clone, Debug)]
162pub struct EngineSszProxyLayer<Api = ()> {
163    handle: EngineSszProxyHandle<Api>,
164}
165
166impl<Api> EngineSszProxyLayer<Api> {
167    /// Creates a new proxy layer and a handle for setting the engine after node launch.
168    pub fn new() -> (Self, EngineSszProxyHandle<Api>) {
169        let handle = EngineSszProxyHandle::new();
170        (Self { handle: handle.clone() }, handle)
171    }
172
173    /// Creates a new proxy layer with an Engine API implementation.
174    pub fn with_engine_api(engine_api: Api) -> (Self, EngineSszProxyHandle<Api>) {
175        let handle = EngineSszProxyHandle::with_engine_api(engine_api);
176        (Self { handle: handle.clone() }, handle)
177    }
178}
179
180impl<S, Api> Layer<S> for EngineSszProxyLayer<Api> {
181    type Service = EngineSszProxyService<S, Api>;
182
183    fn layer(&self, inner: S) -> Self::Service {
184        EngineSszProxyService { inner, handle: self.handle.clone() }
185    }
186}
187
188/// The service produced by [`EngineSszProxyLayer`].
189#[derive(Clone, Debug)]
190pub struct EngineSszProxyService<S, Api = ()> {
191    inner: S,
192    handle: EngineSszProxyHandle<Api>,
193}
194
195impl<S, Api> Service<HttpRequest> for EngineSszProxyService<S, Api>
196where
197    S: Service<HttpRequest, Response = HttpResponse, Error = BoxError> + Send + Clone,
198    S::Future: Send,
199    Api: EngineSszApi,
200{
201    type Response = HttpResponse;
202    type Error = BoxError;
203    type Future = Either<S::Future, BoxFuture<'static, Result<HttpResponse, BoxError>>>;
204
205    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
206        self.inner.poll_ready(cx)
207    }
208
209    fn call(&mut self, request: HttpRequest) -> Self::Future {
210        if !request.uri().path().starts_with("/engine/") {
211            return Either::Left(self.inner.call(request))
212        }
213
214        let handle = self.handle.clone();
215        Either::Right(Box::pin(async move { Ok(handle_engine_ssz_request(handle, request).await) }))
216    }
217}
218
219/// Fork selector used by SSZ Engine API request handling.
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum EngineSszFork {
222    /// Paris fork.
223    Paris,
224    /// Shanghai fork.
225    Shanghai,
226    /// Cancun fork.
227    Cancun,
228    /// Prague fork.
229    Prague,
230    /// Osaka fork.
231    Osaka,
232    /// Amsterdam fork.
233    Amsterdam,
234}
235
236impl EngineSszFork {
237    const fn payloads_version(self) -> u8 {
238        match self {
239            Self::Paris => 1,
240            Self::Shanghai => 2,
241            Self::Cancun => 3,
242            Self::Prague | Self::Osaka => 4,
243            Self::Amsterdam => 5,
244        }
245    }
246
247    const fn forkchoice_version(self) -> u8 {
248        match self {
249            Self::Paris => 1,
250            Self::Shanghai => 2,
251            Self::Cancun | Self::Prague | Self::Osaka => 3,
252            Self::Amsterdam => 4,
253        }
254    }
255}
256
257impl std::str::FromStr for EngineSszFork {
258    type Err = ();
259
260    fn from_str(value: &str) -> Result<Self, Self::Err> {
261        match value {
262            "paris" => Ok(Self::Paris),
263            "shanghai" => Ok(Self::Shanghai),
264            "cancun" => Ok(Self::Cancun),
265            "prague" => Ok(Self::Prague),
266            "osaka" => Ok(Self::Osaka),
267            "amsterdam" => Ok(Self::Amsterdam),
268            _ => Err(()),
269        }
270    }
271}
272
273/// API surface required by the SSZ Engine API proxy.
274///
275/// Custom APIs can opt in with a marker implementation; every route, including
276/// `/capabilities`, then answers 404 so clients fall back to the JSON-RPC Engine API.
277pub trait EngineSszApi: Clone + Send + Sync + 'static {
278    /// Returns the capabilities advertisement.
279    ///
280    /// `witness_enabled` is true when a witness generator is configured and
281    /// [`Self::supports_witness`] holds, so the advertisement can include the extension.
282    fn capabilities(&self, _witness_enabled: bool) -> HttpResponse {
283        problem_response(STATUS_NOT_FOUND, "method-not-found", None)
284    }
285
286    /// Whether the implementation supports the witness extension.
287    fn supports_witness(&self) -> bool {
288        false
289    }
290
291    /// Handles the experimental Amsterdam payload submission with a witness.
292    fn new_payload_with_witness(
293        &self,
294        _body: Bytes,
295        _witness_handler: Arc<dyn EngineSszWitness>,
296    ) -> impl Future<Output = HttpResponse> + Send {
297        async { problem_response(STATUS_NOT_FOUND, "method-not-found", None) }
298    }
299
300    /// Returns the Engine API identity response.
301    fn identity(&self) -> HttpResponse {
302        problem_response(STATUS_NOT_FOUND, "method-not-found", None)
303    }
304
305    /// Handles a new payload request.
306    fn new_payload(
307        &self,
308        _fork: EngineSszFork,
309        _body: Bytes,
310    ) -> impl Future<Output = HttpResponse> + Send {
311        async { problem_response(STATUS_NOT_FOUND, "method-not-found", None) }
312    }
313
314    /// Handles a getPayload request.
315    fn get_payload(
316        &self,
317        _fork: EngineSszFork,
318        _payload_id: PayloadId,
319    ) -> impl Future<Output = HttpResponse> + Send {
320        async { problem_response(STATUS_NOT_FOUND, "method-not-found", None) }
321    }
322
323    /// Handles a forkchoice update request.
324    fn forkchoice_updated(
325        &self,
326        _fork: EngineSszFork,
327        _body: Bytes,
328    ) -> impl Future<Output = HttpResponse> + Send {
329        async { problem_response(STATUS_NOT_FOUND, "method-not-found", None) }
330    }
331
332    /// Handles a getPayloadBodiesByHash request.
333    fn get_payload_bodies_by_hash(
334        &self,
335        _fork: EngineSszFork,
336        _hashes: Vec<B256>,
337    ) -> impl Future<Output = HttpResponse> + Send {
338        async { problem_response(STATUS_NOT_FOUND, "method-not-found", None) }
339    }
340
341    /// Handles a getPayloadBodiesByRange request.
342    fn get_payload_bodies_by_range(
343        &self,
344        _fork: EngineSszFork,
345        _start: u64,
346        _count: u64,
347    ) -> impl Future<Output = HttpResponse> + Send {
348        async { problem_response(STATUS_NOT_FOUND, "method-not-found", None) }
349    }
350
351    /// Handles a getBlobs request.
352    fn get_blobs(&self, _version: u8, _body: Bytes) -> impl Future<Output = HttpResponse> + Send {
353        async { problem_response(STATUS_NOT_FOUND, "method-not-found", None) }
354    }
355}
356
357impl EngineSszApi for reth_node_builder::rpc::NoopEngineApi {}
358
359impl<Provider, Pool, Validator, ChainSpec> EngineSszApi
360    for EthEngineApi<Provider, Pool, Validator, ChainSpec>
361where
362    Provider: BlockReader + StateProviderFactory + BalProvider + 'static,
363    Pool: TransactionPool + 'static,
364    Validator: EngineApiValidator<EthEngineTypes>,
365    ChainSpec: EthereumHardforks + Send + Sync + 'static,
366{
367    fn capabilities(&self, witness_enabled: bool) -> HttpResponse {
368        handle_capabilities(witness_enabled)
369    }
370
371    fn identity(&self) -> HttpResponse {
372        json_response(vec![self.client_version().clone()])
373    }
374
375    async fn new_payload(&self, fork: EngineSszFork, body: Bytes) -> HttpResponse {
376        let payload = match decode_new_payload_request(fork, &body) {
377            Ok(payload) => payload,
378            Err(err) => return err.into_response(),
379        };
380
381        match submit_payload(self, fork, payload).await {
382            Ok(status) => ssz_response(status),
383            Err(response) => response,
384        }
385    }
386
387    fn supports_witness(&self) -> bool {
388        true
389    }
390
391    async fn new_payload_with_witness(
392        &self,
393        body: Bytes,
394        witness_handler: Arc<dyn EngineSszWitness>,
395    ) -> HttpResponse {
396        let payload = match decode_new_payload_request(EngineSszFork::Amsterdam, &body) {
397            Ok(payload) => payload,
398            Err(err) => return err.into_response(),
399        };
400        let status = match submit_payload(self, EngineSszFork::Amsterdam, payload.clone()).await {
401            Ok(status) => status,
402            Err(response) => return response,
403        };
404        let witness = match status.status {
405            PayloadStatusKind::Valid => match witness_handler.generate_witness(payload).await {
406                Ok(witness) => Some(witness),
407                Err(err) => {
408                    return problem_response(
409                        STATUS_INTERNAL_SERVER_ERROR,
410                        "internal",
411                        Some(err.to_string()),
412                    )
413                }
414            },
415            _ => None,
416        };
417        ssz_response(PayloadStatusWithWitness::new(status, witness))
418    }
419
420    async fn get_payload(&self, fork: EngineSszFork, payload_id: PayloadId) -> HttpResponse {
421        match fork {
422            EngineSszFork::Paris => match self.get_payload_v2_metered(payload_id).await {
423                Ok(payload) => {
424                    let block_value = payload.block_value;
425                    match payload.execution_payload {
426                        ExecutionPayloadFieldV2::V1(payload) => {
427                            get_payload_response(BuiltPayloadParis { payload, block_value })
428                        }
429                        ExecutionPayloadFieldV2::V2(_) => {
430                            problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
431                        }
432                    }
433                }
434                Err(err) => engine_error_response(err),
435            },
436            EngineSszFork::Shanghai => match self.get_payload_v2_metered(payload_id).await {
437                Ok(payload) => match BuiltPayloadShanghai::try_from(payload) {
438                    Ok(payload) => get_payload_response(payload),
439                    Err(err) => problem_response(
440                        STATUS_UNPROCESSABLE_ENTITY,
441                        "invalid-body",
442                        Some(err.to_string()),
443                    ),
444                },
445                Err(err) => engine_error_response(err),
446            },
447            EngineSszFork::Cancun => self
448                .get_payload_v3_metered(payload_id)
449                .await
450                .map_or_else(engine_error_response, get_payload_response),
451            EngineSszFork::Prague => self
452                .get_payload_v4_metered(payload_id)
453                .await
454                .map(BuiltPayloadPrague::from)
455                .map_or_else(engine_error_response, get_payload_response),
456            EngineSszFork::Osaka => self
457                .get_payload_v5_metered(payload_id)
458                .await
459                .map(BuiltPayloadOsaka::from)
460                .map_or_else(engine_error_response, get_payload_response),
461            EngineSszFork::Amsterdam => self
462                .get_payload_v6_metered(payload_id)
463                .await
464                .map(BuiltPayloadAmsterdam::from)
465                .map_or_else(engine_error_response, get_payload_response),
466        }
467    }
468
469    async fn forkchoice_updated(&self, fork: EngineSszFork, body: Bytes) -> HttpResponse {
470        let (state, attrs, custody_columns) = match decode_forkchoice_request(fork, &body) {
471            Ok(request) => request,
472            Err(_) => return problem_response(STATUS_BAD_REQUEST, "ssz-decode-error", None),
473        };
474
475        if attrs.as_ref().is_some_and(|attrs| {
476            !timestamp_matches_fork(self.chain_spec().as_ref(), fork, attrs.timestamp)
477        }) {
478            return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
479        }
480
481        let response = match fork.forkchoice_version() {
482            1 => self.fork_choice_updated_v1_metered(state, attrs).await,
483            2 => self.fork_choice_updated_v2_metered(state, attrs).await,
484            3 => self.fork_choice_updated_v3_metered(state, attrs).await,
485            4 => self.fork_choice_updated_v4_metered(state, attrs, custody_columns).await,
486            _ => return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None),
487        };
488
489        match response {
490            Ok(updated) => match ForkchoiceUpdateResponse::try_from(updated) {
491                Ok(updated) => ssz_response(updated),
492                Err(err) => problem_response(
493                    STATUS_INTERNAL_SERVER_ERROR,
494                    "internal",
495                    Some(err.to_string()),
496                ),
497            },
498            Err(err) => engine_error_response(err),
499        }
500    }
501
502    async fn get_payload_bodies_by_hash(
503        &self,
504        fork: EngineSszFork,
505        hashes: Vec<B256>,
506    ) -> HttpResponse {
507        handle_get_payload_bodies(self.clone(), fork, PayloadBodiesRequest::Hash(hashes)).await
508    }
509
510    async fn get_payload_bodies_by_range(
511        &self,
512        fork: EngineSszFork,
513        start: u64,
514        count: u64,
515    ) -> HttpResponse {
516        handle_get_payload_bodies(self.clone(), fork, PayloadBodiesRequest::Range { start, count })
517            .await
518    }
519
520    async fn get_blobs(&self, version: u8, body: Bytes) -> HttpResponse {
521        if version == 4 {
522            let request = match BlobsV4Request::from_ssz_bytes(&body) {
523                Ok(request) => request,
524                Err(_) => return problem_response(STATUS_BAD_REQUEST, "ssz-decode-error", None),
525            };
526            if request.versioned_hashes.len() > MAX_BLOBS_REQUEST {
527                return problem_response(STATUS_PAYLOAD_TOO_LARGE, "request-too-large", None)
528            }
529            return blob_response::<BlobsV4Response, _>(
530                self.get_blobs_v4_metered(request.versioned_hashes, request.indices_bitarray),
531            )
532        }
533        let request = match BlobsV1Request::from_ssz_bytes(&body) {
534            Ok(request) => request,
535            Err(_) => return problem_response(STATUS_BAD_REQUEST, "ssz-decode-error", None),
536        };
537        if request.versioned_hashes.len() > MAX_BLOBS_REQUEST {
538            return problem_response(STATUS_PAYLOAD_TOO_LARGE, "request-too-large", None)
539        }
540        let hashes = request.versioned_hashes;
541        match version {
542            1 => blob_response::<BlobsV1Response, _>(self.get_blobs_v1_metered(hashes).map(Some)),
543            2 => blob_response::<BlobsV2Response, _>(self.get_blobs_v2_metered(hashes)),
544            3 => blob_response::<BlobsV3Response, _>(self.get_blobs_v3_metered(hashes)),
545            _ => problem_response(STATUS_NOT_FOUND, "method-not-found", None),
546        }
547    }
548}
549
550struct EngineSszState<Api> {
551    engine_api: Option<Api>,
552    witness_handler: Option<Arc<dyn EngineSszWitness>>,
553    witness_enabled: bool,
554}
555
556impl<Api: EngineSszApi> EngineSszState<Api> {
557    fn update_witness_support(&mut self) {
558        self.witness_enabled = self.witness_handler.is_some() &&
559            self.engine_api.as_ref().is_some_and(EngineSszApi::supports_witness);
560    }
561}
562
563async fn handle_engine_ssz_request<Api>(
564    handle: EngineSszProxyHandle<Api>,
565    request: HttpRequest,
566) -> HttpResponse
567where
568    Api: EngineSszApi,
569{
570    let Some(endpoint) = parse_engine_path(request.uri().path()) else {
571        return problem_response(STATUS_NOT_FOUND, "method-not-found", None)
572    };
573
574    if request.method() != endpoint.method() {
575        return problem_response(STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", None)
576    }
577
578    match endpoint {
579        EngineSszEndpoint::Capabilities => {
580            let Some(engine_api) = handle.engine_api().await else {
581                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
582            };
583            engine_api.capabilities(handle.witness_enabled().await)
584        }
585        EngineSszEndpoint::Identity => {
586            let Some(engine_api) = handle.engine_api().await else {
587                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
588            };
589            engine_api.identity()
590        }
591        EngineSszEndpoint::NewPayload => {
592            let Some(fork) = request_fork(&request) else {
593                return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
594            };
595            let body = match read_ssz_body(request, MAX_PAYLOAD_BYTES).await {
596                Ok(body) => body,
597                Err(response) => return response,
598            };
599            let Some(engine_api) = handle.engine_api().await else {
600                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
601            };
602            engine_api.new_payload(fork, body).await
603        }
604        EngineSszEndpoint::PayloadsWithWitness => {
605            let Some(fork) = request_fork(&request) else {
606                return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
607            };
608            if fork != EngineSszFork::Amsterdam {
609                return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
610            }
611            let body = match read_ssz_body(request, MAX_PAYLOAD_BYTES).await {
612                Ok(body) => body,
613                Err(response) => return response,
614            };
615            let Some(engine_api) = handle.engine_api().await else {
616                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
617            };
618            let Some(witness_handler) = handle.witness_handler().await else {
619                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
620            };
621            engine_api.new_payload_with_witness(body, witness_handler).await
622        }
623        EngineSszEndpoint::GetPayload(payload_id) => {
624            let Ok(payload_id) = payload_id else {
625                return problem_response(STATUS_BAD_REQUEST, "invalid-request", None)
626            };
627            let Some(fork) = request_fork(&request) else {
628                return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
629            };
630            let Some(engine_api) = handle.engine_api().await else {
631                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
632            };
633            engine_api.get_payload(fork, payload_id).await
634        }
635        EngineSszEndpoint::Forkchoice => {
636            let Some(fork) = request_fork(&request) else {
637                return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
638            };
639            let body = match read_ssz_body(request, MAX_PAYLOAD_BYTES).await {
640                Ok(body) => body,
641                Err(response) => return response,
642            };
643            let Some(engine_api) = handle.engine_api().await else {
644                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
645            };
646            engine_api.forkchoice_updated(fork, body).await
647        }
648        EngineSszEndpoint::PayloadBodiesByHash => {
649            let Some(fork) = request_fork(&request) else {
650                return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
651            };
652            let body = match read_ssz_body(request, MAX_BODIES_REQUEST_BYTES).await {
653                Ok(body) => body,
654                Err(response) => return response,
655            };
656            let request = match BodiesByHashRequest::from_ssz_bytes(&body) {
657                Ok(request) if request.block_hashes.len() <= MAX_BODIES_REQUEST => {
658                    request.block_hashes
659                }
660                Ok(_) => {
661                    return problem_response(STATUS_PAYLOAD_TOO_LARGE, "request-too-large", None)
662                }
663                Err(_) => return problem_response(STATUS_BAD_REQUEST, "ssz-decode-error", None),
664            };
665            let Some(engine_api) = handle.engine_api().await else {
666                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
667            };
668            engine_api.get_payload_bodies_by_hash(fork, request).await
669        }
670        EngineSszEndpoint::PayloadBodiesByRange => {
671            let Some(fork) = request_fork(&request) else {
672                return problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)
673            };
674            let (start, count) =
675                match parse_bodies_range_query(request.uri().query().unwrap_or_default()) {
676                    Ok(range) => range,
677                    Err(response) => return response,
678                };
679            let Some(engine_api) = handle.engine_api().await else {
680                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
681            };
682            engine_api.get_payload_bodies_by_range(fork, start, count).await
683        }
684        EngineSszEndpoint::Blobs(version) => {
685            let body = match read_ssz_body(request, MAX_BLOB_REQUEST_BYTES).await {
686                Ok(body) => body,
687                Err(response) => return response,
688            };
689            let Some(engine_api) = handle.engine_api().await else {
690                return problem_response(STATUS_SERVICE_UNAVAILABLE, "service-unavailable", None)
691            };
692            engine_api.get_blobs(version, body).await
693        }
694    }
695}
696
697fn parse_method_version(version: &str) -> Option<u8> {
698    version.strip_prefix('v')?.parse().ok().filter(|version| (1..=4).contains(version))
699}
700
701fn request_fork(request: &HttpRequest) -> Option<EngineSszFork> {
702    request.headers().get(ETH_EXECUTION_VERSION)?.to_str().ok()?.parse().ok()
703}
704
705fn parse_engine_path(path: &str) -> Option<EngineSszEndpoint> {
706    let mut segments = path.trim_start_matches('/').split('/');
707    match (segments.next(), segments.next(), segments.next(), segments.next(), segments.next()) {
708        (Some("engine"), Some("v1"), Some("capabilities"), None, None) => {
709            Some(EngineSszEndpoint::Capabilities)
710        }
711        (Some("engine"), Some("v1"), Some("identity"), None, None) => {
712            Some(EngineSszEndpoint::Identity)
713        }
714        (Some("engine"), Some("v1"), Some("payloads"), None, None) => {
715            Some(EngineSszEndpoint::NewPayload)
716        }
717        (Some("engine"), Some("v1"), Some("payloads"), Some("witness"), None) => {
718            Some(EngineSszEndpoint::PayloadsWithWitness)
719        }
720        (Some("engine"), Some("v1"), Some("payloads"), Some(payload_id), None) => {
721            let payload_id = payload_id.parse::<PayloadId>();
722            Some(EngineSszEndpoint::GetPayload(payload_id))
723        }
724        (Some("engine"), Some("v1"), Some("forkchoice"), None, None) => {
725            Some(EngineSszEndpoint::Forkchoice)
726        }
727        (Some("engine"), Some("v1"), Some("blobs"), version, None) => {
728            Some(EngineSszEndpoint::Blobs(parse_method_version(version?)?))
729        }
730        (Some("engine"), Some("v1"), Some("bodies"), Some("hash"), None) => {
731            Some(EngineSszEndpoint::PayloadBodiesByHash)
732        }
733        (Some("engine"), Some("v1"), Some("bodies"), None, None) => {
734            Some(EngineSszEndpoint::PayloadBodiesByRange)
735        }
736        _ => None,
737    }
738}
739
740#[derive(Clone, Copy, Debug, Eq, PartialEq)]
741enum EngineSszEndpoint {
742    Capabilities,
743    Identity,
744    NewPayload,
745    PayloadsWithWitness,
746    GetPayload(Result<PayloadId, <PayloadId as std::str::FromStr>::Err>),
747    Forkchoice,
748    PayloadBodiesByHash,
749    PayloadBodiesByRange,
750    Blobs(u8),
751}
752
753impl EngineSszEndpoint {
754    const fn method(&self) -> &'static str {
755        match self {
756            Self::Capabilities |
757            Self::Identity |
758            Self::GetPayload(_) |
759            Self::PayloadBodiesByRange => "GET",
760            Self::NewPayload |
761            Self::PayloadsWithWitness |
762            Self::Forkchoice |
763            Self::PayloadBodiesByHash |
764            Self::Blobs(_) => "POST",
765        }
766    }
767}
768
769fn handle_capabilities(witness_enabled: bool) -> HttpResponse {
770    let mut fork_scoped_endpoints = vec!["payloads", "forkchoice", "bodies"];
771    if witness_enabled {
772        fork_scoped_endpoints.push("payloads/witness");
773    }
774    json_response(serde_json::json!({
775        "supported_forks": ["paris", "shanghai", "cancun", "prague", "osaka", "amsterdam"],
776        "fork_scoped_endpoints": fork_scoped_endpoints,
777        "independently_versioned": {
778            "blobs": ["v1", "v2", "v3", "v4"],
779        },
780        "unscoped_endpoints": ["capabilities", "identity"],
781        "limits": {
782            "bodies.max_count": MAX_BODIES_REQUEST,
783            "blobs.max_versioned_hashes": MAX_BLOBS_REQUEST,
784            "payload.max_bytes": MAX_PAYLOAD_BYTES,
785        },
786    }))
787}
788
789async fn submit_payload<Provider, Pool, Validator, ChainSpec>(
790    engine_api: &EthEngineApi<Provider, Pool, Validator, ChainSpec>,
791    fork: EngineSszFork,
792    payload: ExecutionData,
793) -> Result<EngineSszPayloadStatus, HttpResponse>
794where
795    Provider: BlockReader + StateProviderFactory + BalProvider + 'static,
796    Pool: TransactionPool + 'static,
797    Validator: EngineApiValidator<EthEngineTypes>,
798    ChainSpec: EthereumHardforks + Send + Sync + 'static,
799{
800    let mut status = match fork.payloads_version() {
801        1 => engine_api.new_payload_v1(payload).await,
802        2 => engine_api.new_payload_v2(payload).await,
803        3 => engine_api.new_payload_v3(payload).await,
804        4 => engine_api.new_payload_v4(payload).await,
805        5 => engine_api.new_payload_v5(payload).await,
806        _ => return Err(problem_response(STATUS_BAD_REQUEST, "unsupported-fork", None)),
807    }
808    .map_err(engine_error_response)?;
809    // The SSZ error string is bounded, truncate it rather than losing the INVALID verdict.
810    if let PayloadStatusEnum::Invalid { validation_error } = &mut status.status {
811        validation_error.truncate(validation_error.floor_char_boundary(MAX_ERROR_BYTES));
812    }
813    EngineSszPayloadStatus::try_from(status).map_err(|error| {
814        problem_response(STATUS_INTERNAL_SERVER_ERROR, "internal", Some(error.to_string()))
815    })
816}
817
818fn engine_error_response(err: EngineApiError) -> HttpResponse {
819    let detail = err.to_string();
820    let error: jsonrpsee::types::ErrorObjectOwned = err.into();
821    let (status, problem_type) = match error.code() {
822        -32700 => (STATUS_BAD_REQUEST, "parse-error"),
823        -32600 => (STATUS_BAD_REQUEST, "invalid-request"),
824        -32601 => (STATUS_NOT_FOUND, "method-not-found"),
825        -32602 => (STATUS_UNPROCESSABLE_ENTITY, "invalid-body"),
826        -38001 => (STATUS_NOT_FOUND, "unknown-payload"),
827        -38002 => (STATUS_CONFLICT, "invalid-forkchoice"),
828        -38003 => (STATUS_UNPROCESSABLE_ENTITY, "invalid-attributes"),
829        -38004 => (STATUS_PAYLOAD_TOO_LARGE, "request-too-large"),
830        -38005 => (STATUS_BAD_REQUEST, "unsupported-fork"),
831        -38006 => (STATUS_CONFLICT, "reorg-too-deep"),
832        _ => (STATUS_INTERNAL_SERVER_ERROR, "internal"),
833    };
834    problem_response(status, problem_type, Some(detail))
835}
836
837enum PayloadBodiesRequest {
838    Hash(Vec<B256>),
839    Range { start: u64, count: u64 },
840}
841
842async fn handle_get_payload_bodies<Provider, Pool, Validator, ChainSpec>(
843    engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>,
844    fork: EngineSszFork,
845    request: PayloadBodiesRequest,
846) -> HttpResponse
847where
848    Provider: BlockReader + StateProviderFactory + BalProvider + 'static,
849    Pool: TransactionPool + 'static,
850    Validator: EngineApiValidator<EthEngineTypes>,
851    ChainSpec: EthereumHardforks + Send + Sync + 'static,
852{
853    let include_bal = fork == EngineSszFork::Amsterdam;
854    let response = match request {
855        PayloadBodiesRequest::Hash(hashes) => {
856            engine_api.get_payload_bodies_by_hash_with_timestamps_metered(hashes, include_bal).await
857        }
858        PayloadBodiesRequest::Range { start, count } => {
859            engine_api
860                .get_payload_bodies_by_range_with_timestamps_metered(start, count, include_bal)
861                .await
862        }
863    };
864    let chain_spec = engine_api.chain_spec().as_ref();
865    match fork {
866        EngineSszFork::Amsterdam => payload_bodies_http_response(
867            response,
868            |body| ExecutionPayloadBodyAmsterdam::try_from(body).ok(),
869            fork,
870            chain_spec,
871        ),
872        EngineSszFork::Paris => payload_bodies_http_response(
873            response,
874            |body| ExecutionPayloadBodyParis::try_from(ExecutionPayloadBodyV1::from(body)).ok(),
875            fork,
876            chain_spec,
877        ),
878        _ => payload_bodies_http_response(
879            response,
880            |body| ExecutionPayloadBodyShanghai::try_from(ExecutionPayloadBodyV1::from(body)).ok(),
881            fork,
882            chain_spec,
883        ),
884    }
885}
886
887fn parse_bodies_range_query(query: &str) -> Result<(u64, u64), HttpResponse> {
888    let invalid = || problem_response(STATUS_BAD_REQUEST, "invalid-request", None);
889    let mut start = None;
890    let mut count = None;
891    for pair in query.split('&') {
892        let (key, value) = pair.split_once('=').ok_or_else(invalid)?;
893        let field = match key {
894            "from" if start.is_none() => &mut start,
895            "count" if count.is_none() => &mut count,
896            _ => return Err(invalid()),
897        };
898        *field = Some(value.parse::<u64>().map_err(|_| invalid())?);
899    }
900    let range = (start.ok_or_else(invalid)?, count.ok_or_else(invalid)?);
901    if range.1 > MAX_BODIES_REQUEST as u64 {
902        return Err(problem_response(STATUS_PAYLOAD_TOO_LARGE, "request-too-large", None))
903    }
904    Ok(range)
905}
906
907/// Amsterdam bodies with a missing or pruned BAL are unavailable, just like pruned blocks.
908fn payload_bodies_response<LegacyBody, ForkBody>(
909    response: Result<Vec<Option<(u64, LegacyBody)>>, EngineApiError>,
910    convert: impl Fn(LegacyBody) -> Option<ForkBody>,
911    fork: EngineSszFork,
912    chain_spec: &impl EthereumHardforks,
913) -> Result<BodiesResponse<ForkBody>, HttpResponse>
914where
915    ForkBody: Default + ssz::Encode + ssz::Decode,
916{
917    let bodies = response.map_err(engine_error_response)?;
918    let bodies = bodies
919        .into_iter()
920        .map(|body| {
921            body.filter(|(timestamp, _)| timestamp_matches_fork(chain_spec, fork, *timestamp))
922                .map(|(_, body)| body)
923        })
924        .collect();
925    BodiesResponse::from_optional_bodies(bodies, convert).map_err(|error| {
926        problem_response(STATUS_INTERNAL_SERVER_ERROR, "internal", Some(error.to_string()))
927    })
928}
929
930fn payload_bodies_http_response<LegacyBody, ForkBody>(
931    response: Result<Vec<Option<(u64, LegacyBody)>>, EngineApiError>,
932    convert: impl Fn(LegacyBody) -> Option<ForkBody>,
933    fork: EngineSszFork,
934    chain_spec: &impl EthereumHardforks,
935) -> HttpResponse
936where
937    ForkBody: Default + ssz::Encode + ssz::Decode,
938{
939    match payload_bodies_response(response, convert, fork, chain_spec) {
940        Ok(response) => ssz_response(response),
941        Err(response) => response,
942    }
943}
944
945fn timestamp_matches_fork<ChainSpec: EthereumHardforks>(
946    chain_spec: &ChainSpec,
947    fork: EngineSszFork,
948    timestamp: u64,
949) -> bool {
950    let active = |fork| chain_spec.is_ethereum_fork_active_at_timestamp(fork, timestamp);
951    match fork {
952        EngineSszFork::Paris => !active(EthereumHardfork::Shanghai),
953        EngineSszFork::Shanghai => {
954            active(EthereumHardfork::Shanghai) && !active(EthereumHardfork::Cancun)
955        }
956        EngineSszFork::Cancun => {
957            active(EthereumHardfork::Cancun) && !active(EthereumHardfork::Prague)
958        }
959        EngineSszFork::Prague => {
960            active(EthereumHardfork::Prague) && !active(EthereumHardfork::Osaka)
961        }
962        EngineSszFork::Osaka => {
963            active(EthereumHardfork::Osaka) && !active(EthereumHardfork::Amsterdam)
964        }
965        EngineSszFork::Amsterdam => active(EthereumHardfork::Amsterdam),
966    }
967}
968
969async fn read_ssz_body(request: HttpRequest, max_bytes: usize) -> Result<Bytes, HttpResponse> {
970    let content_type = request.headers().get(CONTENT_TYPE).and_then(|value| value.to_str().ok());
971    if content_type != Some(OCTET_STREAM) {
972        return Err(problem_response(STATUS_UNSUPPORTED_MEDIA_TYPE, "unsupported-media-type", None))
973    }
974
975    if let Some(content_length) = request.headers().get("content-length") {
976        let Some(content_length) =
977            content_length.to_str().ok().and_then(|value| value.parse::<usize>().ok())
978        else {
979            return Err(problem_response(STATUS_BAD_REQUEST, "invalid-request", None))
980        };
981        if content_length > max_bytes {
982            return Err(problem_response(STATUS_PAYLOAD_TOO_LARGE, "request-too-large", None))
983        }
984    }
985
986    match Limited::new(request.into_body(), max_bytes).collect().await {
987        Ok(body) => Ok(body.to_bytes().into()),
988        Err(err) if err.downcast_ref::<LengthLimitError>().is_some() => {
989            Err(problem_response(STATUS_PAYLOAD_TOO_LARGE, "request-too-large", None))
990        }
991        Err(_) => Err(problem_response(STATUS_BAD_REQUEST, "invalid-request", None)),
992    }
993}
994
995fn blob_response<Ssz, Legacy>(response: Result<Option<Legacy>, EngineApiError>) -> HttpResponse
996where
997    Ssz: TryFrom<Legacy> + ssz::Encode,
998    Ssz::Error: std::fmt::Display,
999{
1000    match response {
1001        Ok(Some(response)) => match Ssz::try_from(response) {
1002            Ok(response) => ssz_response(response),
1003            Err(err) => {
1004                problem_response(STATUS_INTERNAL_SERVER_ERROR, "internal", Some(err.to_string()))
1005            }
1006        },
1007        Ok(None) => no_content_response(),
1008        Err(err) => engine_error_response(err),
1009    }
1010}
1011
1012/// Structural SSZ errors and invalid values have distinct REST error codes.
1013#[derive(Debug)]
1014enum PayloadDecodeError {
1015    Ssz(ssz::DecodeError),
1016    InvalidTransaction(alloy_eips::eip2718::Eip2718Error),
1017}
1018
1019impl From<ssz::DecodeError> for PayloadDecodeError {
1020    fn from(error: ssz::DecodeError) -> Self {
1021        Self::Ssz(error)
1022    }
1023}
1024
1025impl PayloadDecodeError {
1026    fn into_response(self) -> HttpResponse {
1027        match self {
1028            Self::Ssz(error) => {
1029                problem_response(STATUS_BAD_REQUEST, "ssz-decode-error", Some(format!("{error:?}")))
1030            }
1031            Self::InvalidTransaction(error) => problem_response(
1032                STATUS_UNPROCESSABLE_ENTITY,
1033                "invalid-body",
1034                Some(error.to_string()),
1035            ),
1036        }
1037    }
1038}
1039
1040fn check_ssz_bound(actual: usize, max: usize, field: &str) -> Result<(), ssz::DecodeError> {
1041    if actual > max {
1042        return Err(ssz::DecodeError::BytesInvalid(format!(
1043            "{field} exceeds SSZ bound: {actual} > {max}"
1044        )))
1045    }
1046    Ok(())
1047}
1048
1049fn decode_new_payload_request(
1050    fork: EngineSszFork,
1051    body: &[u8],
1052) -> Result<ExecutionData, PayloadDecodeError> {
1053    let (payload, parent_root, requests): (ExecutionPayload, _, Option<Requests>) = match fork {
1054        EngineSszFork::Paris => {
1055            let envelope = ExecutionPayloadEnvelopeParis::from_ssz_bytes(body)?;
1056            (envelope.payload.into(), None, None)
1057        }
1058        EngineSszFork::Shanghai => {
1059            let envelope = ExecutionPayloadEnvelopeShanghai::from_ssz_bytes(body)?;
1060            (envelope.payload.into(), None, None)
1061        }
1062        EngineSszFork::Cancun => {
1063            let envelope = ExecutionPayloadEnvelopeCancun::from_ssz_bytes(body)?;
1064            (envelope.payload.into(), Some(envelope.parent_beacon_block_root), None)
1065        }
1066        EngineSszFork::Prague => {
1067            let envelope = ExecutionPayloadEnvelopePrague::from_ssz_bytes(body)?;
1068            (
1069                envelope.payload.into(),
1070                Some(envelope.parent_beacon_block_root),
1071                Some(envelope.execution_requests),
1072            )
1073        }
1074        EngineSszFork::Osaka => {
1075            let envelope = ExecutionPayloadEnvelopeOsaka::from_ssz_bytes(body)?;
1076            (
1077                envelope.payload.into(),
1078                Some(envelope.parent_beacon_block_root),
1079                Some(envelope.execution_requests),
1080            )
1081        }
1082        EngineSszFork::Amsterdam => {
1083            let envelope = ExecutionPayloadEnvelopeAmsterdam::from_ssz_bytes(body)?;
1084            (
1085                ExecutionPayload::V4(envelope.payload),
1086                Some(envelope.parent_beacon_block_root),
1087                Some(envelope.execution_requests),
1088            )
1089        }
1090    };
1091
1092    // Alloy's codecs reproduce the wire layout but do not enforce the SSZ list bounds.
1093    // Check those independently of the smaller, advertised total HTTP request limit.
1094    let inner = payload.as_v1();
1095    check_ssz_bound(inner.extra_data.len(), 32, "extra_data")?;
1096    check_ssz_bound(inner.transactions.len(), 1 << 20, "transactions")?;
1097    for transaction in &inner.transactions {
1098        check_ssz_bound(transaction.len(), 1 << 30, "transaction")?;
1099    }
1100    if let Some(withdrawals) = payload.withdrawals() {
1101        check_ssz_bound(withdrawals.len(), 16, "withdrawals")?;
1102    }
1103    if let Some(bal) = payload.block_access_list() {
1104        check_ssz_bound(bal.len(), 1 << 30, "block_access_list")?;
1105    }
1106    if let Some(requests) = &requests {
1107        check_ssz_bound(requests.len(), 256, "execution_requests")?;
1108        for request in requests.iter() {
1109            check_ssz_bound(request.len(), 1 << 30, "execution_request")?;
1110        }
1111    }
1112
1113    let versioned_hashes = calculate_versioned_hashes(&payload)?;
1114    let sidecar = match parent_root {
1115        Some(parent_beacon_block_root) => {
1116            let cancun = CancunPayloadFields { parent_beacon_block_root, versioned_hashes };
1117            match requests {
1118                Some(requests) => {
1119                    ExecutionPayloadSidecar::v4(cancun, PraguePayloadFields::new(requests))
1120                }
1121                None => ExecutionPayloadSidecar::v3(cancun),
1122            }
1123        }
1124        None => ExecutionPayloadSidecar::none(),
1125    };
1126    Ok(ExecutionData::new(payload, sidecar))
1127}
1128
1129fn calculate_versioned_hashes(payload: &ExecutionPayload) -> Result<Vec<B256>, PayloadDecodeError> {
1130    let mut versioned_hashes = Vec::new();
1131    for transaction in payload.decoded_transactions::<TxEnvelope>() {
1132        let transaction = transaction.map_err(PayloadDecodeError::InvalidTransaction)?;
1133        if let Some(hashes) = transaction.blob_versioned_hashes() {
1134            versioned_hashes.extend_from_slice(hashes);
1135        }
1136    }
1137    Ok(versioned_hashes)
1138}
1139
1140fn decode_forkchoice_request(
1141    fork: EngineSszFork,
1142    body: &[u8],
1143) -> Result<(ForkchoiceState, Option<PayloadAttributes>, Option<B128>), ssz::DecodeError> {
1144    let (state, attrs, custody) = match fork {
1145        EngineSszFork::Paris => {
1146            let ForkchoiceUpdateParis { forkchoice_state, payload_attributes } =
1147                ForkchoiceUpdateParis::from_ssz_bytes(body)?;
1148            (forkchoice_state, optional_attrs(payload_attributes), None)
1149        }
1150        EngineSszFork::Shanghai => {
1151            let ForkchoiceUpdateShanghai { forkchoice_state, payload_attributes } =
1152                ForkchoiceUpdateShanghai::from_ssz_bytes(body)?;
1153            (forkchoice_state, optional_attrs(payload_attributes), None)
1154        }
1155        EngineSszFork::Cancun => {
1156            let ForkchoiceUpdateCancun { forkchoice_state, payload_attributes } =
1157                ForkchoiceUpdateCancun::from_ssz_bytes(body)?;
1158            (forkchoice_state, optional_attrs(payload_attributes), None)
1159        }
1160        EngineSszFork::Prague => {
1161            let ForkchoiceUpdatePrague { forkchoice_state, payload_attributes } =
1162                ForkchoiceUpdatePrague::from_ssz_bytes(body)?;
1163            (forkchoice_state, optional_attrs(payload_attributes), None)
1164        }
1165        EngineSszFork::Osaka => {
1166            let ForkchoiceUpdateOsaka { forkchoice_state, payload_attributes } =
1167                ForkchoiceUpdateOsaka::from_ssz_bytes(body)?;
1168            (forkchoice_state, optional_attrs(payload_attributes), None)
1169        }
1170        EngineSszFork::Amsterdam => {
1171            let ForkchoiceUpdateAmsterdam { forkchoice_state, payload_attributes, custody_columns } =
1172                ForkchoiceUpdateAmsterdam::from_ssz_bytes(body)?;
1173            (forkchoice_state, optional_attrs(payload_attributes), custody_columns.into_option())
1174        }
1175    };
1176    if let Some(withdrawals) =
1177        attrs.as_ref().and_then(|attrs: &PayloadAttributes| attrs.withdrawals.as_ref())
1178    {
1179        check_ssz_bound(withdrawals.len(), 16, "withdrawals")?;
1180    }
1181    Ok((state, attrs, custody))
1182}
1183
1184fn optional_attrs<T>(attrs: Optional<T>) -> Option<PayloadAttributes>
1185where
1186    T: Into<PayloadAttributes>,
1187{
1188    attrs.into_option().map(Into::into)
1189}
1190
1191fn ssz_response<T: ssz::Encode>(value: T) -> HttpResponse {
1192    HttpResponse::builder()
1193        .status(STATUS_OK)
1194        .header(CONTENT_TYPE, OCTET_STREAM)
1195        .body(HttpBody::from(value.as_ssz_bytes()))
1196        .expect("valid response")
1197}
1198
1199fn get_payload_response<T: ssz::Encode>(value: T) -> HttpResponse {
1200    let mut response = ssz_response(value);
1201    response.headers_mut().insert(CACHE_CONTROL, "no-store".parse().expect("valid cache control"));
1202    response
1203}
1204
1205fn json_response<T: serde::Serialize>(value: T) -> HttpResponse {
1206    let Ok(body) = serde_json::to_string(&value) else {
1207        return problem_response(
1208            STATUS_INTERNAL_SERVER_ERROR,
1209            "internal",
1210            Some("failed to encode json".to_string()),
1211        )
1212    };
1213
1214    HttpResponse::builder()
1215        .status(STATUS_OK)
1216        .header(CONTENT_TYPE, APPLICATION_JSON)
1217        .body(HttpBody::from(body))
1218        .expect("valid response")
1219}
1220
1221fn no_content_response() -> HttpResponse {
1222    HttpResponse::builder().status(204).body(HttpBody::empty()).expect("valid response")
1223}
1224
1225fn problem_response(
1226    status: u16,
1227    problem_type: &'static str,
1228    detail: Option<String>,
1229) -> HttpResponse {
1230    let problem_type = format!("/engine-api/errors/{problem_type}");
1231    let body = match detail {
1232        Some(detail) => serde_json::json!({ "type": problem_type, "detail": detail }),
1233        None => serde_json::json!({ "type": problem_type }),
1234    };
1235
1236    HttpResponse::builder()
1237        .status(status)
1238        .header(CONTENT_TYPE, PROBLEM_JSON)
1239        .body(HttpBody::from(body.to_string()))
1240        .expect("valid response")
1241}
1242
1243#[cfg(test)]
1244mod tests {
1245    use super::*;
1246    use crate::{engine_ssz_witness::EngineSszWitnessError, EthereumEngineValidator};
1247    use alloy_rpc_types_engine::{
1248        ssz_engine_types::{
1249            PayloadAttributesAmsterdam, PayloadAttributesCancun, PayloadAttributesParis,
1250            PayloadAttributesShanghai,
1251        },
1252        ClientCode, ClientVersionV1, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3,
1253        ForkchoiceUpdateError, PayloadStatus,
1254    };
1255    use reth_chainspec::{ChainSpecBuilder, ForkCondition, MAINNET};
1256    use reth_engine_primitives::{
1257        BeaconEngineMessage, BeaconForkChoiceUpdateError, ConsensusEngineHandle,
1258    };
1259    use reth_ethereum_primitives::Block;
1260    use reth_network_api::noop::NoopNetwork;
1261    use reth_payload_builder::PayloadBuilderHandle;
1262    use reth_payload_primitives::PayloadBuilderError;
1263    use reth_storage_api::noop::NoopProvider;
1264    use reth_tasks::Runtime;
1265    use reth_transaction_pool::noop::NoopTransactionPool;
1266    use ssz::Encode;
1267    use tokio::sync::mpsc::unbounded_channel;
1268
1269    #[tokio::test]
1270    async fn witness_capabilities_follow_both_wiring_orders() {
1271        #[derive(Clone)]
1272        struct Api(bool);
1273        impl EngineSszApi for Api {
1274            fn supports_witness(&self) -> bool {
1275                self.0
1276            }
1277        }
1278        struct Witness;
1279        impl EngineSszWitness for Witness {
1280            fn generate_witness(
1281                &self,
1282                _: ExecutionData,
1283            ) -> BoxFuture<
1284                'static,
1285                Result<crate::engine_ssz_witness::ExecutionWitnessV1, EngineSszWitnessError>,
1286            > {
1287                Box::pin(async { Ok(Default::default()) })
1288            }
1289        }
1290        let handle = EngineSszProxyHandle::new();
1291        handle.set_witness_handler_sync(Arc::new(Witness));
1292        assert!(!handle.witness_enabled().await);
1293        handle.set_engine_api_sync(Api(true));
1294        assert!(handle.witness_enabled().await);
1295        handle.set_engine_api(Api(false)).await;
1296        assert!(!handle.witness_enabled().await);
1297
1298        let handle = EngineSszProxyHandle::with_engine_api(Api(true));
1299        assert!(!handle.witness_enabled().await);
1300        handle.set_witness_handler(Arc::new(Witness)).await;
1301        assert!(handle.witness_enabled().await);
1302    }
1303
1304    #[tokio::test]
1305    async fn witness_is_only_advertised_when_configured() {
1306        for enabled in [false, true] {
1307            let body = handle_capabilities(enabled).into_body().collect().await.unwrap().to_bytes();
1308            let capabilities: serde_json::Value = serde_json::from_slice(&body).unwrap();
1309            let endpoints = capabilities["fork_scoped_endpoints"].as_array().unwrap();
1310            assert_eq!(endpoints.iter().any(|endpoint| endpoint == "payloads/witness"), enabled);
1311        }
1312        assert_eq!(
1313            parse_engine_path("/engine/v1/payloads/witness"),
1314            Some(EngineSszEndpoint::PayloadsWithWitness)
1315        );
1316    }
1317
1318    #[test]
1319    fn payload_schema_bounds() {
1320        let mut payload = ExecutionPayloadV3 {
1321            payload_inner: ExecutionPayloadV2 {
1322                payload_inner: ExecutionPayloadV1::from_block_unchecked(
1323                    B256::ZERO,
1324                    &reth_ethereum_primitives::Block::default(),
1325                ),
1326                withdrawals: vec![],
1327            },
1328            blob_gas_used: 0,
1329            excess_blob_gas: 0,
1330        };
1331        payload.payload_inner.payload_inner.extra_data = vec![0; 32].into();
1332        payload.payload_inner.withdrawals = vec![Default::default(); 16];
1333        let mut envelope = ExecutionPayloadEnvelopePrague {
1334            payload,
1335            parent_beacon_block_root: B256::ZERO,
1336            execution_requests: Requests::new(vec![Bytes::new(); 256]),
1337        };
1338        assert!(decode_new_payload_request(EngineSszFork::Prague, &envelope.as_ssz_bytes()).is_ok());
1339        envelope.execution_requests = Requests::new(vec![Bytes::new(); 257]);
1340        assert!(matches!(
1341            decode_new_payload_request(EngineSszFork::Prague, &envelope.as_ssz_bytes()),
1342            Err(PayloadDecodeError::Ssz(_))
1343        ));
1344        envelope.execution_requests = Requests::default();
1345        envelope.payload.payload_inner.withdrawals.push(Default::default());
1346        assert!(matches!(
1347            decode_new_payload_request(EngineSszFork::Prague, &envelope.as_ssz_bytes()),
1348            Err(PayloadDecodeError::Ssz(_))
1349        ));
1350        envelope.payload.payload_inner.withdrawals.clear();
1351        envelope.payload.payload_inner.payload_inner.extra_data = vec![0; 33].into();
1352        assert!(matches!(
1353            decode_new_payload_request(EngineSszFork::Prague, &envelope.as_ssz_bytes()),
1354            Err(PayloadDecodeError::Ssz(_))
1355        ));
1356        let shanghai = ExecutionPayloadEnvelopeShanghai {
1357            payload: ExecutionPayloadV2 {
1358                payload_inner: ExecutionPayloadV1::from_block_unchecked(
1359                    B256::ZERO,
1360                    &reth_ethereum_primitives::Block::default(),
1361                ),
1362                withdrawals: vec![Default::default(); 17],
1363            },
1364        };
1365        assert!(matches!(
1366            decode_new_payload_request(EngineSszFork::Shanghai, &shanghai.as_ssz_bytes()),
1367            Err(PayloadDecodeError::Ssz(_))
1368        ));
1369        let paris = ExecutionPayloadEnvelopeParis {
1370            payload: ExecutionPayloadV1 {
1371                transactions: vec![Bytes::new(); (1 << 20) + 1],
1372                ..ExecutionPayloadV1::from_block_unchecked(
1373                    B256::ZERO,
1374                    &reth_ethereum_primitives::Block::default(),
1375                )
1376            },
1377        };
1378        assert!(matches!(
1379            decode_new_payload_request(EngineSszFork::Paris, &paris.as_ssz_bytes()),
1380            Err(PayloadDecodeError::Ssz(_))
1381        ));
1382    }
1383
1384    #[tokio::test]
1385    async fn new_payload_keeps_invalid_status_with_long_validation_error() {
1386        let (to_engine, mut engine_rx) = unbounded_channel();
1387        let chain_spec = MAINNET.clone();
1388        let api = EngineApi::new(
1389            NoopProvider::default(),
1390            chain_spec.clone(),
1391            ConsensusEngineHandle::new(to_engine),
1392            PayloadBuilderHandle::new(unbounded_channel().0).into(),
1393            NoopTransactionPool::default(),
1394            Runtime::test(),
1395            ClientVersionV1 {
1396                code: ClientCode::RH,
1397                name: "Reth".to_string(),
1398                version: String::new(),
1399                commit: String::new(),
1400            },
1401            Default::default(),
1402            EthereumEngineValidator::new(chain_spec),
1403            false,
1404            NoopNetwork::default(),
1405        );
1406        let envelope = ExecutionPayloadEnvelopeParis {
1407            payload: ExecutionPayloadV1::from_block_unchecked(B256::ZERO, &Block::default()),
1408        };
1409        // `€` is 3 bytes, so `MAX_ERROR_BYTES` falls inside a character.
1410        let validation_error = "€".repeat(MAX_ERROR_BYTES);
1411
1412        let (response, ()) = tokio::join!(
1413            api.new_payload(EngineSszFork::Paris, envelope.as_ssz_bytes().into()),
1414            async {
1415                let Some(BeaconEngineMessage::NewPayload { tx, .. }) = engine_rx.recv().await
1416                else {
1417                    panic!("engine did not receive the payload");
1418                };
1419                let status = PayloadStatusEnum::Invalid { validation_error };
1420                tx.send(Ok(PayloadStatus::from_status(status))).unwrap();
1421            }
1422        );
1423
1424        assert_eq!(response.status(), STATUS_OK);
1425        let body = response.into_body().collect().await.unwrap().to_bytes();
1426        let status = EngineSszPayloadStatus::from_ssz_bytes(&body).unwrap();
1427        assert_eq!(
1428            PayloadStatus::from(status),
1429            PayloadStatus::from_status(PayloadStatusEnum::Invalid {
1430                validation_error: "€".repeat(MAX_ERROR_BYTES / 3),
1431            })
1432        );
1433    }
1434
1435    #[test]
1436    fn forkchoice_withdrawal_bounds() {
1437        for count in [16, 17] {
1438            let withdrawals = vec![Default::default(); count];
1439            let state = ForkchoiceState::default();
1440            let shanghai = ForkchoiceUpdateShanghai {
1441                forkchoice_state: state,
1442                payload_attributes: Optional::some(PayloadAttributesShanghai {
1443                    withdrawals: withdrawals.clone(),
1444                    ..Default::default()
1445                }),
1446            };
1447            let cancun = ForkchoiceUpdateCancun {
1448                forkchoice_state: state,
1449                payload_attributes: Optional::some(PayloadAttributesCancun {
1450                    withdrawals: withdrawals.clone(),
1451                    ..Default::default()
1452                }),
1453            };
1454            let amsterdam = ForkchoiceUpdateAmsterdam {
1455                forkchoice_state: state,
1456                payload_attributes: Optional::some(PayloadAttributesAmsterdam {
1457                    withdrawals,
1458                    ..Default::default()
1459                }),
1460                custody_columns: Optional::none(),
1461            };
1462            for (fork, bytes) in [
1463                (EngineSszFork::Shanghai, shanghai.as_ssz_bytes()),
1464                (EngineSszFork::Cancun, cancun.as_ssz_bytes()),
1465                (EngineSszFork::Prague, cancun.as_ssz_bytes()),
1466                (EngineSszFork::Osaka, cancun.as_ssz_bytes()),
1467                (EngineSszFork::Amsterdam, amsterdam.as_ssz_bytes()),
1468            ] {
1469                assert_eq!(
1470                    decode_forkchoice_request(fork, &bytes).is_ok(),
1471                    count == 16,
1472                    "{fork:?}"
1473                );
1474            }
1475        }
1476    }
1477
1478    #[test]
1479    fn paris_forkchoice_uses_fixed_size_optional_attributes() {
1480        let request = ForkchoiceUpdateParis {
1481            forkchoice_state: ForkchoiceState::default(),
1482            payload_attributes: Optional::some(PayloadAttributesParis {
1483                timestamp: 1_700_000_000,
1484                ..Default::default()
1485            }),
1486        };
1487        let bytes = request.as_ssz_bytes();
1488        assert_eq!(bytes.len(), 160);
1489        assert_eq!(&bytes[96..100], &100u32.to_le_bytes());
1490        assert_eq!(&bytes[100..108], &1_700_000_000u64.to_le_bytes());
1491        assert_eq!(
1492            decode_forkchoice_request(EngineSszFork::Paris, &bytes).unwrap().1.unwrap().timestamp,
1493            1_700_000_000
1494        );
1495    }
1496
1497    #[tokio::test]
1498    async fn engine_errors_preserve_validation_semantics() {
1499        for (error, status, kind) in [
1500            (EngineApiError::UnknownPayload, 404, "unknown-payload"),
1501            (EngineApiError::BlobRequestTooLarge { len: 129 }, 413, "request-too-large"),
1502            (
1503                EngineApiError::ForkChoiceUpdate(
1504                    BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
1505                        ForkchoiceUpdateError::InvalidState,
1506                    ),
1507                ),
1508                409,
1509                "invalid-forkchoice",
1510            ),
1511            (
1512                EngineApiError::ForkChoiceUpdate(
1513                    BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
1514                        ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
1515                    ),
1516                ),
1517                422,
1518                "invalid-attributes",
1519            ),
1520            (
1521                EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::PayloadJob(
1522                    PayloadBuilderError::MissingParentHeader(B256::ZERO),
1523                )),
1524                500,
1525                "internal",
1526            ),
1527        ] {
1528            let response = engine_error_response(error);
1529            assert_eq!(response.status(), status);
1530            assert_eq!(response.headers()[CONTENT_TYPE], PROBLEM_JSON);
1531            let body = response.into_body().collect().await.unwrap().to_bytes();
1532            let problem: serde_json::Value = serde_json::from_slice(&body).unwrap();
1533            assert_eq!(problem["type"], format!("/engine-api/errors/{kind}"));
1534        }
1535    }
1536
1537    #[test]
1538    fn bodies_range_query_validation() {
1539        assert_eq!(parse_bodies_range_query("from=1&count=32").unwrap(), (1, 32));
1540        assert_eq!(parse_bodies_range_query("count=2&from=3").unwrap(), (3, 2));
1541        for query in
1542            ["", "from=1", "from=x&count=1", "from=1&count=1&count=2", "from=1&count=1&other=0"]
1543        {
1544            assert_eq!(parse_bodies_range_query(query).unwrap_err().status(), 400);
1545        }
1546        assert_eq!(parse_bodies_range_query("from=1&count=33").unwrap_err().status(), 413);
1547    }
1548
1549    #[test]
1550    fn payload_bodies_are_filtered_at_fork_boundaries() {
1551        let chain_spec = ChainSpecBuilder::default()
1552            .chain(1.into())
1553            .genesis(Default::default())
1554            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(10))
1555            .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(20))
1556            .with_fork(EthereumHardfork::Prague, ForkCondition::Timestamp(30))
1557            .with_fork(EthereumHardfork::Osaka, ForkCondition::Timestamp(40))
1558            .with_fork(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(50))
1559            .build();
1560        for (fork, start, end) in [
1561            (EngineSszFork::Shanghai, 10, 20),
1562            (EngineSszFork::Cancun, 20, 30),
1563            (EngineSszFork::Prague, 30, 40),
1564            (EngineSszFork::Osaka, 40, 50),
1565        ] {
1566            let body = ExecutionPayloadBodyV1 {
1567                transactions: vec![Bytes::from_static(&[1, 2, 3])],
1568                withdrawals: Some(vec![]),
1569            };
1570            let response = payload_bodies_response(
1571                Ok(vec![
1572                    Some((start - 1, body.clone())),
1573                    Some((start, body.clone())),
1574                    Some((end - 1, body.clone())),
1575                    Some((end, body)),
1576                    None,
1577                ]),
1578                |body| ExecutionPayloadBodyShanghai::try_from(body).ok(),
1579                fork,
1580                &chain_spec,
1581            )
1582            .unwrap();
1583            assert_eq!(
1584                response.entries.iter().map(|entry| entry.available).collect::<Vec<_>>(),
1585                [false, true, true, false, false]
1586            );
1587            for index in [0, 3, 4] {
1588                assert_eq!(response.entries[index].body, ExecutionPayloadBodyShanghai::default());
1589            }
1590        }
1591        assert!(timestamp_matches_fork(&chain_spec, EngineSszFork::Paris, 9));
1592        assert!(!timestamp_matches_fork(&chain_spec, EngineSszFork::Paris, 10));
1593        assert!(!timestamp_matches_fork(&chain_spec, EngineSszFork::Amsterdam, 49));
1594        assert!(timestamp_matches_fork(&chain_spec, EngineSszFork::Amsterdam, 50));
1595    }
1596
1597    #[tokio::test]
1598    async fn payload_bodies_hash_request_limits_and_media_type() {
1599        for content_length in [false, true] {
1600            for count in [32, 33] {
1601                let bytes =
1602                    BodiesByHashRequest { block_hashes: vec![B256::ZERO; count] }.as_ssz_bytes();
1603                let mut request = HttpRequest::builder().header(CONTENT_TYPE, OCTET_STREAM);
1604                if content_length {
1605                    request = request.header("content-length", bytes.len());
1606                }
1607                let result = read_ssz_body(
1608                    request.body(HttpBody::from(bytes)).unwrap(),
1609                    MAX_BODIES_REQUEST_BYTES,
1610                )
1611                .await;
1612                if count == 32 {
1613                    assert_eq!(
1614                        BodiesByHashRequest::from_ssz_bytes(&result.unwrap())
1615                            .unwrap()
1616                            .block_hashes
1617                            .len(),
1618                        32
1619                    );
1620                } else {
1621                    let response = result.unwrap_err();
1622                    assert_eq!(response.status(), STATUS_PAYLOAD_TOO_LARGE);
1623                    assert_eq!(response.headers()[CONTENT_TYPE], PROBLEM_JSON);
1624                }
1625            }
1626        }
1627        let response = read_ssz_body(HttpRequest::new(HttpBody::empty()), MAX_BODIES_REQUEST_BYTES)
1628            .await
1629            .unwrap_err();
1630        assert_eq!(response.status(), STATUS_UNSUPPORTED_MEDIA_TYPE);
1631    }
1632
1633    #[tokio::test]
1634    async fn payload_bodies_errors_and_capabilities_match_rest_spec() {
1635        let response =
1636            engine_error_response(EngineApiError::InvalidBodiesRange { start: 0, count: 1 });
1637        assert_eq!(response.status(), 422);
1638        assert_eq!(response.headers()[CONTENT_TYPE], PROBLEM_JSON);
1639        let body = response.into_body().collect().await.unwrap().to_bytes();
1640        let problem: serde_json::Value = serde_json::from_slice(&body).unwrap();
1641        assert_eq!(problem["type"], "/engine-api/errors/invalid-body");
1642        let body = handle_capabilities(false).into_body().collect().await.unwrap().to_bytes();
1643        let capabilities: serde_json::Value = serde_json::from_slice(&body).unwrap();
1644        assert_eq!(capabilities["limits"]["bodies.max_count"], 32);
1645    }
1646
1647    #[test]
1648    fn parses_capabilities_endpoint() {
1649        let endpoint = parse_engine_path("/engine/v1/capabilities").unwrap();
1650        assert_eq!(endpoint, EngineSszEndpoint::Capabilities);
1651    }
1652
1653    #[test]
1654    fn parses_identity_endpoint() {
1655        let endpoint = parse_engine_path("/engine/v1/identity").unwrap();
1656        assert_eq!(endpoint, EngineSszEndpoint::Identity);
1657    }
1658
1659    #[test]
1660    fn parses_fork_scoped_payload_endpoint() {
1661        let endpoint = parse_engine_path("/engine/v1/payloads").unwrap();
1662        assert_eq!(endpoint, EngineSszEndpoint::NewPayload);
1663    }
1664
1665    #[test]
1666    fn parses_fork_scoped_get_payload_endpoint() {
1667        let endpoint = parse_engine_path("/engine/v1/payloads/0x0000000000000001").unwrap();
1668        assert_eq!(
1669            endpoint,
1670            EngineSszEndpoint::GetPayload(Ok(PayloadId::new([0, 0, 0, 0, 0, 0, 0, 1])))
1671        );
1672    }
1673
1674    #[test]
1675    fn matches_malformed_get_payload_endpoint() {
1676        assert!(matches!(
1677            parse_engine_path("/engine/v1/payloads/0x01"),
1678            Some(EngineSszEndpoint::GetPayload(Err(_)))
1679        ));
1680    }
1681
1682    #[test]
1683    fn parses_fork_scoped_forkchoice_endpoint() {
1684        let endpoint = parse_engine_path("/engine/v1/forkchoice").unwrap();
1685        assert_eq!(endpoint, EngineSszEndpoint::Forkchoice);
1686    }
1687
1688    #[test]
1689    fn parses_payload_bodies_endpoints() {
1690        assert_eq!(
1691            parse_engine_path("/engine/v1/bodies/hash").unwrap(),
1692            EngineSszEndpoint::PayloadBodiesByHash
1693        );
1694        assert_eq!(
1695            parse_engine_path("/engine/v1/bodies").unwrap(),
1696            EngineSszEndpoint::PayloadBodiesByRange
1697        );
1698    }
1699
1700    #[test]
1701    fn rejects_legacy_version_scoped_endpoint() {
1702        assert!(parse_engine_path("/engine/v4/payloads").is_none());
1703    }
1704
1705    #[test]
1706    fn decodes_blob_request_container() {
1707        let hashes = vec![B256::ZERO, B256::with_last_byte(1)];
1708        let decoded = BlobsV1Request::from_ssz_bytes(
1709            &BlobsV1Request { versioned_hashes: hashes.clone() }.as_ssz_bytes(),
1710        )
1711        .unwrap()
1712        .versioned_hashes;
1713        assert_eq!(decoded, hashes);
1714    }
1715
1716    #[test]
1717    fn decodes_forkchoice_v4_with_custody_columns() {
1718        let forkchoice_state = ForkchoiceState::same_hash(B256::ZERO);
1719        let encoded = ForkchoiceUpdateAmsterdam {
1720            forkchoice_state,
1721            payload_attributes: Optional::none(),
1722            custody_columns: Optional::some(B128::with_last_byte(1)),
1723        }
1724        .as_ssz_bytes();
1725
1726        let (decoded_state, decoded_attrs, custody_columns) =
1727            decode_forkchoice_request(EngineSszFork::Amsterdam, &encoded).unwrap();
1728        assert_eq!(decoded_state, forkchoice_state);
1729        assert!(decoded_attrs.is_none());
1730        assert_eq!(custody_columns, Some(B128::with_last_byte(1)));
1731    }
1732
1733    #[test]
1734    fn decodes_forkchoice_cancun_payload_attributes() {
1735        let forkchoice_state = ForkchoiceState::same_hash(B256::ZERO);
1736        let attrs = PayloadAttributesCancun {
1737            timestamp: 1,
1738            prev_randao: B256::with_last_byte(2),
1739            suggested_fee_recipient: Default::default(),
1740            withdrawals: Vec::new(),
1741            parent_beacon_block_root: B256::with_last_byte(3),
1742        };
1743        let encoded =
1744            ForkchoiceUpdateCancun { forkchoice_state, payload_attributes: Optional::some(attrs) }
1745                .as_ssz_bytes();
1746
1747        let (decoded_state, decoded_attrs, custody_columns) =
1748            decode_forkchoice_request(EngineSszFork::Cancun, &encoded).unwrap();
1749        assert_eq!(decoded_state, forkchoice_state);
1750        let decoded_attrs = decoded_attrs.unwrap();
1751        assert_eq!(decoded_attrs.timestamp, 1);
1752        assert!(decoded_attrs.withdrawals.as_ref().unwrap().is_empty());
1753        assert_eq!(decoded_attrs.parent_beacon_block_root, Some(B256::with_last_byte(3)));
1754        assert!(custody_columns.is_none());
1755    }
1756}