1use 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>;
72pub 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 pub async fn witness_enabled(&self) -> bool {
112 self.state.read().await.witness_enabled
113 }
114
115 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 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 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 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 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 pub async fn engine_api(&self) -> Option<Api> {
156 self.state.read().await.engine_api.clone()
157 }
158}
159
160#[derive(Clone, Debug)]
162pub struct EngineSszProxyLayer<Api = ()> {
163 handle: EngineSszProxyHandle<Api>,
164}
165
166impl<Api> EngineSszProxyLayer<Api> {
167 pub fn new() -> (Self, EngineSszProxyHandle<Api>) {
169 let handle = EngineSszProxyHandle::new();
170 (Self { handle: handle.clone() }, handle)
171 }
172
173 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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum EngineSszFork {
222 Paris,
224 Shanghai,
226 Cancun,
228 Prague,
230 Osaka,
232 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
273pub trait EngineSszApi: Clone + Send + Sync + 'static {
278 fn capabilities(&self, _witness_enabled: bool) -> HttpResponse {
283 problem_response(STATUS_NOT_FOUND, "method-not-found", None)
284 }
285
286 fn supports_witness(&self) -> bool {
288 false
289 }
290
291 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 fn identity(&self) -> HttpResponse {
302 problem_response(STATUS_NOT_FOUND, "method-not-found", None)
303 }
304
305 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 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 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 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 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 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 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
907fn 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#[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 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 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}