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 alloy_consensus::{Transaction, TxEnvelope};
8use alloy_eips::{
9    eip2718::Decodable2718,
10    eip7685::{Requests, RequestsOrHash},
11};
12use alloy_primitives::{Bytes, B128, B256};
13use alloy_rpc_types_engine::{
14    CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadSidecar,
15    ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ExecutionPayloadV4,
16    ForkchoiceState, PayloadAttributes, PraguePayloadFields,
17};
18use http_body_util::BodyExt;
19use jsonrpsee::server::{HttpBody, HttpRequest, HttpResponse};
20use reth_chainspec::EthereumHardforks;
21use reth_engine_primitives::EngineApiValidator;
22use reth_ethereum_engine_primitives::EthEngineTypes;
23use reth_provider::{BalProvider, BlockReader, HeaderProvider, StateProviderFactory};
24use reth_rpc::EngineApi;
25use reth_transaction_pool::TransactionPool;
26use ssz::Decode;
27use std::{
28    future::Future,
29    pin::Pin,
30    sync::Arc,
31    task::{Context, Poll},
32};
33use tokio::sync::RwLock;
34use tower::{BoxError, Layer, Service};
35
36const OCTET_STREAM: &str = "application/octet-stream";
37const APPLICATION_JSON: &str = "application/json";
38const TEXT_PLAIN: &str = "text/plain";
39const CONTENT_TYPE: &str = "content-type";
40const ETH_EXECUTION_VERSION: &str = "eth-execution-version";
41
42const STATUS_OK: u16 = 200;
43const STATUS_BAD_REQUEST: u16 = 400;
44const STATUS_NOT_FOUND: u16 = 404;
45const STATUS_METHOD_NOT_ALLOWED: u16 = 405;
46const STATUS_INTERNAL_SERVER_ERROR: u16 = 500;
47const STATUS_SERVICE_UNAVAILABLE: u16 = 503;
48
49const MAX_BLOB_LIMIT: usize = 128;
50const MAX_PAYLOAD_BYTES: u64 = 64 * 1024 * 1024;
51
52type EthEngineApi<Provider, Pool, Validator, ChainSpec> =
53    EngineApi<Provider, EthEngineTypes, Pool, Validator, ChainSpec>;
54type SharedEthEngineApi<Provider, Pool, Validator, ChainSpec> =
55    Arc<RwLock<Option<EthEngineApi<Provider, Pool, Validator, ChainSpec>>>>;
56
57/// Shared handle used by [`EngineSszProxyLayer`].
58pub struct EngineSszProxyHandle<ChainSpec, Provider = (), Pool = (), Validator = ()> {
59    engine_api: SharedEthEngineApi<Provider, Pool, Validator, ChainSpec>,
60}
61
62impl<C, Provider, Pool, Validator> Clone for EngineSszProxyHandle<C, Provider, Pool, Validator> {
63    fn clone(&self) -> Self {
64        Self { engine_api: self.engine_api.clone() }
65    }
66}
67
68impl<C, Provider, Pool, Validator> std::fmt::Debug
69    for EngineSszProxyHandle<C, Provider, Pool, Validator>
70{
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("EngineSszProxyHandle").finish_non_exhaustive()
73    }
74}
75
76impl<ChainSpec, Provider, Pool, Validator>
77    EngineSszProxyHandle<ChainSpec, Provider, Pool, Validator>
78{
79    fn new() -> Self {
80        Self { engine_api: Default::default() }
81    }
82
83    fn with_engine_api(engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>) -> Self {
84        Self { engine_api: Arc::new(RwLock::new(Some(engine_api))) }
85    }
86
87    /// Sets the Engine API implementation used by the proxy.
88    pub async fn set_engine_api(
89        &self,
90        engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>,
91    ) {
92        *self.engine_api.write().await = Some(engine_api);
93    }
94
95    /// Sets the Engine API implementation during synchronous launch wiring.
96    pub fn set_engine_api_sync(
97        &self,
98        engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>,
99    ) {
100        *self
101            .engine_api
102            .try_write()
103            .expect("engine api handle should not be locked during launch") = Some(engine_api);
104    }
105}
106
107impl<ChainSpec, Provider, Pool, Validator>
108    EngineSszProxyHandle<ChainSpec, Provider, Pool, Validator>
109{
110    /// Returns the Engine API implementation used by the proxy.
111    pub async fn engine_api(&self) -> Option<EthEngineApi<Provider, Pool, Validator, ChainSpec>> {
112        self.engine_api.read().await.clone()
113    }
114}
115
116/// A tower layer that intercepts SSZ Engine API routes under `/engine/v1`.
117#[derive(Clone, Debug)]
118pub struct EngineSszProxyLayer<ChainSpec, Provider = (), Pool = (), Validator = ()> {
119    handle: EngineSszProxyHandle<ChainSpec, Provider, Pool, Validator>,
120}
121
122impl<ChainSpec, Provider, Pool, Validator>
123    EngineSszProxyLayer<ChainSpec, Provider, Pool, Validator>
124{
125    /// Creates a new proxy layer and a handle for setting the engine after node launch.
126    pub fn new() -> (Self, EngineSszProxyHandle<ChainSpec, Provider, Pool, Validator>) {
127        let handle = EngineSszProxyHandle::new();
128        (Self { handle: handle.clone() }, handle)
129    }
130
131    /// Creates a new proxy layer with an Engine API implementation.
132    pub fn with_engine_api(
133        engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>,
134    ) -> (Self, EngineSszProxyHandle<ChainSpec, Provider, Pool, Validator>) {
135        let handle = EngineSszProxyHandle::with_engine_api(engine_api);
136        (Self { handle: handle.clone() }, handle)
137    }
138}
139
140impl<S, ChainSpec, Provider, Pool, Validator> Layer<S>
141    for EngineSszProxyLayer<ChainSpec, Provider, Pool, Validator>
142{
143    type Service = EngineSszProxyService<S, ChainSpec, Provider, Pool, Validator>;
144
145    fn layer(&self, inner: S) -> Self::Service {
146        EngineSszProxyService { inner, handle: self.handle.clone() }
147    }
148}
149
150/// The service produced by [`EngineSszProxyLayer`].
151#[derive(Clone, Debug)]
152pub struct EngineSszProxyService<S, ChainSpec, Provider = (), Pool = (), Validator = ()> {
153    inner: S,
154    handle: EngineSszProxyHandle<ChainSpec, Provider, Pool, Validator>,
155}
156
157impl<S, ChainSpec, Provider, Pool, Validator> Service<HttpRequest>
158    for EngineSszProxyService<S, ChainSpec, Provider, Pool, Validator>
159where
160    S: Service<HttpRequest, Response = HttpResponse, Error = BoxError> + Send + Clone,
161    S::Future: Send + 'static,
162    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
163    Pool: TransactionPool + 'static,
164    Validator: EngineApiValidator<EthEngineTypes>,
165    ChainSpec: EthereumHardforks + Send + Sync + 'static,
166{
167    type Response = HttpResponse;
168    type Error = BoxError;
169    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
170
171    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
172        self.inner.poll_ready(cx)
173    }
174
175    fn call(&mut self, request: HttpRequest) -> Self::Future {
176        if !request.uri().path().starts_with("/engine/") {
177            let fut = self.inner.call(request);
178            return Box::pin(fut)
179        }
180
181        let handle = self.handle.clone();
182        Box::pin(async move { Ok(handle_engine_ssz_request(handle, request).await) })
183    }
184}
185
186async fn handle_engine_ssz_request<ChainSpec, Provider, Pool, Validator>(
187    handle: EngineSszProxyHandle<ChainSpec, Provider, Pool, Validator>,
188    request: HttpRequest,
189) -> HttpResponse
190where
191    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
192    Pool: TransactionPool + 'static,
193    Validator: EngineApiValidator<EthEngineTypes>,
194    ChainSpec: EthereumHardforks + Send + Sync + 'static,
195{
196    let method = request.method().as_str().to_owned();
197    let path = request.uri().path().to_owned();
198    let Some(endpoint) = parse_engine_path(&path) else {
199        return text_response(STATUS_NOT_FOUND, "unknown engine ssz endpoint")
200    };
201
202    match endpoint {
203        EngineSszEndpoint::Capabilities => {
204            if method != "GET" {
205                return text_response(STATUS_METHOD_NOT_ALLOWED, "method not allowed")
206            }
207            handle_capabilities()
208        }
209        EngineSszEndpoint::Identity => {
210            if method != "GET" {
211                return text_response(STATUS_METHOD_NOT_ALLOWED, "method not allowed")
212            }
213            let Some(engine_api) = handle.engine_api().await else {
214                return text_response(STATUS_SERVICE_UNAVAILABLE, "engine api unavailable")
215            };
216            handle_identity(engine_api)
217        }
218        EngineSszEndpoint::NewPayload => {
219            if method != "POST" {
220                return text_response(STATUS_METHOD_NOT_ALLOWED, "method not allowed")
221            }
222            let Some(fork) = request_fork(&request) else {
223                return text_response(STATUS_BAD_REQUEST, "unsupported fork")
224            };
225            let Ok(body) = request.into_body().collect().await.map(|body| body.to_bytes()) else {
226                return text_response(STATUS_BAD_REQUEST, "failed to read request body")
227            };
228            let Some(engine_api) = handle.engine_api().await else {
229                return text_response(STATUS_SERVICE_UNAVAILABLE, "engine api unavailable")
230            };
231            handle_new_payload(engine_api, fork.payloads_version(), &body).await
232        }
233        EngineSszEndpoint::Forkchoice => {
234            if method != "POST" {
235                return text_response(STATUS_METHOD_NOT_ALLOWED, "method not allowed")
236            }
237            let Some(fork) = request_fork(&request) else {
238                return text_response(STATUS_BAD_REQUEST, "unsupported fork")
239            };
240            let Ok(body) = request.into_body().collect().await.map(|body| body.to_bytes()) else {
241                return text_response(STATUS_BAD_REQUEST, "failed to read request body")
242            };
243            let Some(engine_api) = handle.engine_api().await else {
244                return text_response(STATUS_SERVICE_UNAVAILABLE, "engine api unavailable")
245            };
246            handle_forkchoice_updated(engine_api, fork.forkchoice_version(), &body).await
247        }
248        EngineSszEndpoint::Blobs(version) => {
249            if method != "POST" {
250                return text_response(STATUS_METHOD_NOT_ALLOWED, "method not allowed")
251            }
252            let Ok(body) = request.into_body().collect().await.map(|body| body.to_bytes()) else {
253                return text_response(STATUS_BAD_REQUEST, "failed to read request body")
254            };
255            let Some(engine_api) = handle.engine_api().await else {
256                return text_response(STATUS_SERVICE_UNAVAILABLE, "engine api unavailable")
257            };
258            handle_get_blobs(engine_api, version, &body).await
259        }
260    }
261}
262
263fn request_fork(request: &HttpRequest) -> Option<EngineSszFork> {
264    request.headers().get(ETH_EXECUTION_VERSION)?.to_str().ok()?.parse().ok()
265}
266
267fn parse_engine_path(path: &str) -> Option<EngineSszEndpoint> {
268    let mut segments = path.trim_start_matches('/').split('/');
269    match (segments.next(), segments.next(), segments.next(), segments.next(), segments.next()) {
270        (Some("engine"), Some("v1"), Some("capabilities"), None, None) => {
271            Some(EngineSszEndpoint::Capabilities)
272        }
273        (Some("engine"), Some("v1"), Some("identity"), None, None) => {
274            Some(EngineSszEndpoint::Identity)
275        }
276        (Some("engine"), Some("v1"), Some("payloads"), None, None) => {
277            Some(EngineSszEndpoint::NewPayload)
278        }
279        (Some("engine"), Some("v1"), Some("forkchoice"), None, None) => {
280            Some(EngineSszEndpoint::Forkchoice)
281        }
282        (Some("engine"), Some("v1"), Some("blobs"), version, None) => {
283            Some(EngineSszEndpoint::Blobs(parse_method_version(version?)?))
284        }
285        _ => None,
286    }
287}
288
289#[derive(Clone, Copy, Debug, Eq, PartialEq)]
290enum EngineSszEndpoint {
291    Capabilities,
292    Identity,
293    NewPayload,
294    Forkchoice,
295    Blobs(u8),
296}
297
298#[derive(Clone, Copy, Debug, Eq, PartialEq)]
299enum EngineSszFork {
300    Paris,
301    Shanghai,
302    Cancun,
303    Prague,
304    Osaka,
305    Amsterdam,
306}
307
308impl EngineSszFork {
309    const fn payloads_version(self) -> u8 {
310        match self {
311            Self::Paris => 1,
312            Self::Shanghai => 2,
313            Self::Cancun => 3,
314            Self::Prague | Self::Osaka => 4,
315            Self::Amsterdam => 5,
316        }
317    }
318
319    const fn forkchoice_version(self) -> u8 {
320        match self {
321            Self::Paris => 1,
322            Self::Shanghai => 2,
323            Self::Cancun | Self::Prague | Self::Osaka => 3,
324            Self::Amsterdam => 4,
325        }
326    }
327}
328
329impl std::str::FromStr for EngineSszFork {
330    type Err = ();
331
332    fn from_str(value: &str) -> Result<Self, Self::Err> {
333        match value {
334            "paris" => Ok(Self::Paris),
335            "shanghai" => Ok(Self::Shanghai),
336            "cancun" => Ok(Self::Cancun),
337            "prague" => Ok(Self::Prague),
338            "osaka" => Ok(Self::Osaka),
339            "amsterdam" => Ok(Self::Amsterdam),
340            _ => Err(()),
341        }
342    }
343}
344
345fn parse_method_version(version: &str) -> Option<u8> {
346    version.strip_prefix('v')?.parse().ok().filter(|version| (1..=4).contains(version))
347}
348
349fn handle_capabilities() -> HttpResponse {
350    json_response(serde_json::json!({
351        "supported_forks": ["paris", "shanghai", "cancun", "prague", "osaka", "amsterdam"],
352        "fork_scoped_endpoints": ["payloads", "forkchoice", "bodies"],
353        "independently_versioned": {
354            "blobs": ["v1", "v2", "v3", "v4"],
355        },
356        "unscoped_endpoints": ["capabilities", "identity"],
357        "limits": {
358            "bodies.max_count": 128,
359            "blobs.max_versioned_hashes": MAX_BLOB_LIMIT,
360            "payload.max_bytes": MAX_PAYLOAD_BYTES,
361        },
362    }))
363}
364
365fn handle_identity<Provider, Pool, Validator, ChainSpec>(
366    engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>,
367) -> HttpResponse
368where
369    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
370    Pool: TransactionPool + 'static,
371    Validator: EngineApiValidator<EthEngineTypes>,
372    ChainSpec: EthereumHardforks + Send + Sync + 'static,
373{
374    json_response(vec![engine_api.client_version().clone()])
375}
376
377async fn handle_new_payload<Provider, Pool, Validator, ChainSpec>(
378    engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>,
379    version: u8,
380    body: &[u8],
381) -> HttpResponse
382where
383    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
384    Pool: TransactionPool + 'static,
385    Validator: EngineApiValidator<EthEngineTypes>,
386    ChainSpec: EthereumHardforks + Send + Sync + 'static,
387{
388    let payload = match decode_new_payload_request(version, body) {
389        Ok(payload) => payload,
390        Err(err) => return text_response(STATUS_BAD_REQUEST, err),
391    };
392
393    let response = match version {
394        1 => engine_api.new_payload_v1(payload).await,
395        2 => engine_api.new_payload_v2(payload).await,
396        3 => engine_api.new_payload_v3(payload).await,
397        4 => engine_api.new_payload_v4(payload).await,
398        5 => engine_api.new_payload_v5(payload).await,
399        _ => return text_response(STATUS_BAD_REQUEST, "unsupported payload endpoint version"),
400    };
401
402    match response {
403        Ok(status) => ssz_response(status),
404        Err(err) => text_response(STATUS_INTERNAL_SERVER_ERROR, err.to_string()),
405    }
406}
407
408async fn handle_forkchoice_updated<Provider, Pool, Validator, ChainSpec>(
409    engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>,
410    version: u8,
411    body: &[u8],
412) -> HttpResponse
413where
414    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
415    Pool: TransactionPool + 'static,
416    Validator: EngineApiValidator<EthEngineTypes>,
417    ChainSpec: EthereumHardforks + Send + Sync + 'static,
418{
419    let (state, attrs, custody_columns) = match decode_forkchoice_request(version, body) {
420        Ok(request) => request,
421        Err(err) => return text_response(STATUS_BAD_REQUEST, err),
422    };
423
424    let response = match version {
425        1 => engine_api.fork_choice_updated_v1_metered(state, attrs).await,
426        2 => engine_api.fork_choice_updated_v2_metered(state, attrs).await,
427        3 => engine_api.fork_choice_updated_v3_metered(state, attrs).await,
428        4 => engine_api.fork_choice_updated_v4_metered(state, attrs, custody_columns).await,
429        _ => return text_response(STATUS_BAD_REQUEST, "unsupported forkchoice endpoint version"),
430    };
431
432    match response {
433        Ok(updated) => ssz_response(updated),
434        Err(err) => text_response(STATUS_INTERNAL_SERVER_ERROR, err.to_string()),
435    }
436}
437
438/// Handles SSZ `engine_getBlobsV*` requests with the node's blob store.
439async fn handle_get_blobs<ChainSpec, Provider, Pool, Validator>(
440    engine_api: EthEngineApi<Provider, Pool, Validator, ChainSpec>,
441    version: u8,
442    body: &[u8],
443) -> HttpResponse
444where
445    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
446    Pool: TransactionPool + 'static,
447    Validator: EngineApiValidator<EthEngineTypes>,
448    ChainSpec: EthereumHardforks + Send + Sync + 'static,
449{
450    match version {
451        1 => {
452            let hashes = match decode_blob_hashes_request(body) {
453                Ok(hashes) => hashes,
454                Err(err) => return text_response(STATUS_BAD_REQUEST, err),
455            };
456            match engine_api.get_blobs_v1_metered(hashes) {
457                Ok(response) => ssz_response(response),
458                Err(err) => text_response(STATUS_INTERNAL_SERVER_ERROR, err.to_string()),
459            }
460        }
461        2 => {
462            let hashes = match decode_blob_hashes_request(body) {
463                Ok(hashes) => hashes,
464                Err(err) => return text_response(STATUS_BAD_REQUEST, err),
465            };
466            match engine_api.get_blobs_v2_metered(hashes) {
467                Ok(Some(response)) => ssz_response(response),
468                Ok(None) => no_content_response(),
469                Err(err) => text_response(STATUS_INTERNAL_SERVER_ERROR, err.to_string()),
470            }
471        }
472        3 => {
473            let hashes = match decode_blob_hashes_request(body) {
474                Ok(hashes) => hashes,
475                Err(err) => return text_response(STATUS_BAD_REQUEST, err),
476            };
477            match engine_api.get_blobs_v3_metered(hashes) {
478                Ok(Some(response)) => ssz_response(response),
479                Ok(None) => no_content_response(),
480                Err(err) => text_response(STATUS_INTERNAL_SERVER_ERROR, err.to_string()),
481            }
482        }
483        4 => {
484            let (hashes, indices_bitarray) = match decode_blob_cells_request(body) {
485                Ok(request) => request,
486                Err(err) => return text_response(STATUS_BAD_REQUEST, err),
487            };
488            match engine_api.get_blobs_v4_metered(hashes, indices_bitarray) {
489                Ok(Some(response)) => ssz_response(response),
490                Ok(None) => no_content_response(),
491                Err(err) => text_response(STATUS_INTERNAL_SERVER_ERROR, err.to_string()),
492            }
493        }
494        _ => text_response(STATUS_NOT_FOUND, "unsupported blobs endpoint version"),
495    }
496}
497
498/// Decodes the common getBlobs request container with only versioned hashes.
499fn decode_blob_hashes_request(body: &[u8]) -> Result<Vec<B256>, &'static str> {
500    Vec::<B256>::from_ssz_bytes(body).map_err(|_| "invalid ssz")
501}
502
503/// Decodes the Amsterdam getBlobs request container with hashes and a cell index mask.
504fn decode_blob_cells_request(body: &[u8]) -> Result<(Vec<B256>, B128), &'static str> {
505    <(Vec<B256>, B128) as ssz::Decode>::from_ssz_bytes(body).map_err(|_| "invalid ssz")
506}
507
508fn decode_new_payload_request(version: u8, body: &[u8]) -> Result<ExecutionData, &'static str> {
509    match version {
510        1 => {
511            let execution_payload =
512                decode_one::<ExecutionPayloadV1>(body).map_err(|_| "invalid ssz")?;
513            Ok(ExecutionData::new(execution_payload.into(), ExecutionPayloadSidecar::none()))
514        }
515        2 => {
516            let execution_payload =
517                decode_one::<ExecutionPayloadV2>(body).map_err(|_| "invalid ssz")?;
518            Ok(ExecutionData::new(execution_payload.into(), ExecutionPayloadSidecar::none()))
519        }
520        3 => {
521            let (execution_payload, parent_beacon_block_root) =
522                <(ExecutionPayloadV3, B256)>::from_ssz_bytes(body).map_err(|_| "invalid ssz")?;
523            let versioned_hashes = calculate_versioned_hashes(
524                &execution_payload.payload_inner.payload_inner.transactions,
525            )?;
526            let sidecar = ExecutionPayloadSidecar::v3(CancunPayloadFields {
527                parent_beacon_block_root,
528                versioned_hashes,
529            });
530            Ok(ExecutionData::new(execution_payload.into(), sidecar))
531        }
532        4 => {
533            let (execution_payload, parent_beacon_block_root, execution_requests) =
534                <(ExecutionPayloadV3, B256, Vec<Bytes>)>::from_ssz_bytes(body)
535                    .map_err(|_| "invalid ssz")?;
536            let versioned_hashes = calculate_versioned_hashes(
537                &execution_payload.payload_inner.payload_inner.transactions,
538            )?;
539            let sidecar = ExecutionPayloadSidecar::v4(
540                CancunPayloadFields { parent_beacon_block_root, versioned_hashes },
541                PraguePayloadFields::new(RequestsOrHash::Requests(Requests::new(
542                    execution_requests,
543                ))),
544            );
545            Ok(ExecutionData::new(execution_payload.into(), sidecar))
546        }
547        5 => {
548            let (execution_payload, parent_beacon_block_root, execution_requests) =
549                <(ExecutionPayloadV4, B256, Vec<Bytes>)>::from_ssz_bytes(body)
550                    .map_err(|_| "invalid ssz")?;
551            let versioned_hashes = calculate_versioned_hashes(
552                &execution_payload.payload_inner.payload_inner.payload_inner.transactions,
553            )?;
554            let sidecar = ExecutionPayloadSidecar::v4(
555                CancunPayloadFields { parent_beacon_block_root, versioned_hashes },
556                PraguePayloadFields::new(RequestsOrHash::Requests(Requests::new(
557                    execution_requests,
558                ))),
559            );
560            Ok(ExecutionData::new(ExecutionPayload::V4(execution_payload), sidecar))
561        }
562        _ => Err("unsupported payload endpoint version"),
563    }
564}
565
566fn calculate_versioned_hashes(transactions: &[Bytes]) -> Result<Vec<B256>, &'static str> {
567    let mut versioned_hashes = Vec::new();
568    for transaction in transactions {
569        let transaction =
570            TxEnvelope::decode_2718_exact(transaction.as_ref()).map_err(|_| "invalid tx")?;
571        if let Some(hashes) = transaction.blob_versioned_hashes() {
572            versioned_hashes.extend_from_slice(hashes);
573        }
574    }
575
576    Ok(versioned_hashes)
577}
578
579fn decode_forkchoice_request(
580    version: u8,
581    body: &[u8],
582) -> Result<(ForkchoiceState, Option<PayloadAttributes>, Option<B128>), &'static str> {
583    match version {
584        1..=3 => {
585            let (forkchoice_state, payload_attributes) =
586                <(ForkchoiceState, Vec<PayloadAttributes>)>::from_ssz_bytes(body)
587                    .map_err(|_| "invalid ssz")?;
588            Ok((forkchoice_state, payload_attrs(version, payload_attributes)?, None))
589        }
590        4 => {
591            let (forkchoice_state, payload_attributes, custody_columns) =
592                <(ForkchoiceState, Vec<PayloadAttributes>, Vec<B128>)>::from_ssz_bytes(body)
593                    .map_err(|_| "invalid ssz")?;
594            Ok((
595                forkchoice_state,
596                payload_attrs(version, payload_attributes)?,
597                custody_columns_opt(custody_columns)?,
598            ))
599        }
600        _ => Err("unsupported forkchoice endpoint version"),
601    }
602}
603
604fn decode_one<T: ssz::Decode>(body: &[u8]) -> Result<T, ssz::DecodeError> {
605    let mut builder = ssz::SszDecoderBuilder::new(body);
606    builder.register_type::<T>()?;
607    let mut decoder = builder.build()?;
608    decoder.decode_next()
609}
610
611fn payload_attrs(
612    version: u8,
613    attrs: Vec<PayloadAttributes>,
614) -> Result<Option<PayloadAttributes>, &'static str> {
615    if attrs.len() > 1 {
616        return Err("payload_attributes must contain at most one value")
617    }
618
619    attrs.into_iter().next().map(|attrs| validate_payload_attrs_version(version, attrs)).transpose()
620}
621
622fn custody_columns_opt(custody_columns: Vec<B128>) -> Result<Option<B128>, &'static str> {
623    if custody_columns.len() > 1 {
624        return Err("invalid params")
625    }
626
627    Ok(custody_columns.into_iter().next())
628}
629
630fn validate_payload_attrs_version(
631    version: u8,
632    attrs: PayloadAttributes,
633) -> Result<PayloadAttributes, &'static str> {
634    let matches_version = match version {
635        1 => {
636            attrs.withdrawals.is_none() &&
637                attrs.parent_beacon_block_root.is_none() &&
638                attrs.slot_number.is_none()
639        }
640        2 => {
641            attrs.withdrawals.is_some() &&
642                attrs.parent_beacon_block_root.is_none() &&
643                attrs.slot_number.is_none()
644        }
645        3 => {
646            attrs.withdrawals.is_some() &&
647                attrs.parent_beacon_block_root.is_some() &&
648                attrs.slot_number.is_none()
649        }
650        4 => {
651            attrs.withdrawals.is_some() &&
652                attrs.parent_beacon_block_root.is_some() &&
653                attrs.slot_number.is_some()
654        }
655        _ => false,
656    };
657
658    if matches_version {
659        Ok(attrs)
660    } else {
661        Err("payload_attributes version does not match endpoint")
662    }
663}
664
665fn ssz_response<T: ssz::Encode>(value: T) -> HttpResponse {
666    HttpResponse::builder()
667        .status(STATUS_OK)
668        .header(CONTENT_TYPE, OCTET_STREAM)
669        .body(HttpBody::from(value.as_ssz_bytes()))
670        .expect("valid response")
671}
672
673fn json_response<T: serde::Serialize>(value: T) -> HttpResponse {
674    let Ok(body) = serde_json::to_string(&value) else {
675        return text_response(STATUS_INTERNAL_SERVER_ERROR, "failed to encode json")
676    };
677
678    HttpResponse::builder()
679        .status(STATUS_OK)
680        .header(CONTENT_TYPE, APPLICATION_JSON)
681        .body(HttpBody::from(body))
682        .expect("valid response")
683}
684
685fn no_content_response() -> HttpResponse {
686    HttpResponse::builder().status(204).body(HttpBody::empty()).expect("valid response")
687}
688
689fn text_response(status: u16, body: impl Into<String>) -> HttpResponse {
690    HttpResponse::builder()
691        .status(status)
692        .header(CONTENT_TYPE, TEXT_PLAIN)
693        .body(HttpBody::from(body.into()))
694        .expect("valid response")
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use ssz::Encode;
701
702    #[test]
703    fn parses_capabilities_endpoint() {
704        let endpoint = parse_engine_path("/engine/v1/capabilities").unwrap();
705        assert_eq!(endpoint, EngineSszEndpoint::Capabilities);
706    }
707
708    #[test]
709    fn parses_identity_endpoint() {
710        let endpoint = parse_engine_path("/engine/v1/identity").unwrap();
711        assert_eq!(endpoint, EngineSszEndpoint::Identity);
712    }
713
714    #[test]
715    fn parses_fork_scoped_payload_endpoint() {
716        let endpoint = parse_engine_path("/engine/v1/payloads").unwrap();
717        assert_eq!(endpoint, EngineSszEndpoint::NewPayload);
718    }
719
720    #[test]
721    fn parses_fork_scoped_forkchoice_endpoint() {
722        let endpoint = parse_engine_path("/engine/v1/forkchoice").unwrap();
723        assert_eq!(endpoint, EngineSszEndpoint::Forkchoice);
724    }
725
726    #[test]
727    fn rejects_legacy_version_scoped_endpoint() {
728        assert!(parse_engine_path("/engine/v4/payloads").is_none());
729    }
730
731    #[test]
732    fn decodes_top_level_blob_hashes_request() {
733        let hashes = vec![B256::ZERO, B256::with_last_byte(1)];
734        let decoded = decode_blob_hashes_request(&hashes.as_ssz_bytes()).unwrap();
735        assert_eq!(decoded, hashes);
736    }
737
738    #[test]
739    fn decodes_forkchoice_v4_with_custody_columns() {
740        let forkchoice_state = ForkchoiceState {
741            head_block_hash: B256::ZERO,
742            safe_block_hash: B256::ZERO,
743            finalized_block_hash: B256::ZERO,
744        };
745        let encoded =
746            (forkchoice_state, Vec::<PayloadAttributes>::new(), vec![B128::with_last_byte(1)])
747                .as_ssz_bytes();
748
749        let (decoded_state, decoded_attrs, custody_columns) =
750            decode_forkchoice_request(4, &encoded).unwrap();
751        assert_eq!(decoded_state, forkchoice_state);
752        assert!(decoded_attrs.is_none());
753        assert_eq!(custody_columns, Some(B128::with_last_byte(1)));
754    }
755
756    #[test]
757    fn rejects_forkchoice_v4_with_multiple_custody_columns() {
758        let forkchoice_state = ForkchoiceState {
759            head_block_hash: B256::ZERO,
760            safe_block_hash: B256::ZERO,
761            finalized_block_hash: B256::ZERO,
762        };
763        let encoded = (
764            forkchoice_state,
765            Vec::<PayloadAttributes>::new(),
766            vec![B128::ZERO, B128::with_last_byte(1)],
767        )
768            .as_ssz_bytes();
769
770        let err = decode_forkchoice_request(4, &encoded).unwrap_err();
771        assert_eq!(err, "invalid params");
772    }
773}