1use 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
57pub 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 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 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 pub async fn engine_api(&self) -> Option<EthEngineApi<Provider, Pool, Validator, ChainSpec>> {
112 self.engine_api.read().await.clone()
113 }
114}
115
116#[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 pub fn new() -> (Self, EngineSszProxyHandle<ChainSpec, Provider, Pool, Validator>) {
127 let handle = EngineSszProxyHandle::new();
128 (Self { handle: handle.clone() }, handle)
129 }
130
131 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#[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
438async 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
498fn decode_blob_hashes_request(body: &[u8]) -> Result<Vec<B256>, &'static str> {
500 Vec::<B256>::from_ssz_bytes(body).map_err(|_| "invalid ssz")
501}
502
503fn 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}