Skip to main content

reth_rpc_engine_api/
engine_api.rs

1use crate::{
2    capabilities::EngineCapabilities, metrics::EngineApiMetrics, EngineApiError, EngineApiResult,
3};
4use alloy_eips::{
5    eip1898::BlockHashOrNumber,
6    eip4844::{BlobAndProofV1, BlobAndProofV2, BlobCellsAndProofsV1},
7    eip4895::Withdrawals,
8    eip7685::RequestsOrHash,
9};
10use alloy_primitives::{BlockHash, BlockNumber, Bytes, Sealable, B128, B256, U64};
11use alloy_rpc_types_engine::{
12    CancunPayloadFields, ClientVersionV1, ExecutionData, ExecutionPayloadBodiesV1,
13    ExecutionPayloadBodiesV2, ExecutionPayloadBodyV1, ExecutionPayloadBodyV2,
14    ExecutionPayloadInputV2, ExecutionPayloadSidecar, ExecutionPayloadV1, ExecutionPayloadV3,
15    ExecutionPayloadV4, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus,
16    PraguePayloadFields,
17};
18use async_trait::async_trait;
19use jsonrpsee_core::{server::RpcModule, RpcResult};
20use reth_chainspec::EthereumHardforks;
21use reth_engine_primitives::{ConsensusEngineHandle, EngineApiValidator, EngineTypes};
22use reth_network_api::{CellCustody, NetworkInfo};
23use reth_payload_builder::PayloadStore;
24use reth_payload_primitives::{
25    validate_payload_timestamp, EngineApiMessageVersion, MessageValidationKind,
26    PayloadOrAttributes, PayloadTypes,
27};
28use reth_primitives_traits::{Block, BlockBody};
29use reth_rpc_api::{EngineApiServer, IntoEngineApiRpcModule};
30use reth_storage_api::{BalProvider, BlockReader, HeaderProvider, StateProviderFactory};
31use reth_tasks::Runtime;
32use reth_transaction_pool::TransactionPool;
33use std::{
34    sync::Arc,
35    time::{Instant, SystemTime},
36};
37use tokio::sync::oneshot;
38use tracing::{debug, trace, warn};
39
40/// The Engine API response sender.
41pub type EngineApiSender<Ok> = oneshot::Sender<EngineApiResult<Ok>>;
42
43/// The upper limit for payload bodies request.
44const MAX_PAYLOAD_BODIES_LIMIT: u64 = 1024;
45
46/// The upper limit for blobs in `engine_getBlobsVx`.
47const MAX_BLOB_LIMIT: usize = 128;
48
49/// The Engine API implementation that grants the Consensus layer access to data and
50/// functions in the Execution layer that are crucial for the consensus process.
51///
52/// This type is generic over [`EngineTypes`] and intended to be used as the entrypoint for engine
53/// API processing. It can be reused by other non L1 engine APIs that deviate from the L1 spec but
54/// are still follow the engine API model.
55///
56/// ## Implementers
57///
58/// Implementing support for an engine API jsonrpsee RPC handler is done by defining the engine API
59/// server trait and implementing it on a type that can either wrap this [`EngineApi`] type or
60/// use a custom [`EngineTypes`] implementation if it mirrors ethereum's versioned engine API
61/// endpoints (e.g. opstack).
62/// See also [`EngineApiServer`] implementation for this type which is the
63/// L1 implementation.
64pub struct EngineApi<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSpec> {
65    inner: Arc<EngineApiInner<Provider, PayloadT, Pool, Validator, ChainSpec>>,
66}
67
68impl<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSpec>
69    EngineApi<Provider, PayloadT, Pool, Validator, ChainSpec>
70{
71    /// Returns the configured chainspec.
72    pub fn chain_spec(&self) -> &Arc<ChainSpec> {
73        &self.inner.chain_spec
74    }
75
76    /// Returns the configured client version.
77    pub fn client_version(&self) -> &ClientVersionV1 {
78        &self.inner.client
79    }
80}
81
82impl<Provider, PayloadT, Pool, Validator, ChainSpec>
83    EngineApi<Provider, PayloadT, Pool, Validator, ChainSpec>
84where
85    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
86    PayloadT: PayloadTypes,
87    Pool: TransactionPool + 'static,
88    Validator: EngineApiValidator<PayloadT>,
89    ChainSpec: EthereumHardforks + Send + Sync + 'static,
90{
91    /// Create new instance of [`EngineApi`].
92    #[expect(clippy::too_many_arguments)]
93    pub fn new(
94        provider: Provider,
95        chain_spec: Arc<ChainSpec>,
96        beacon_consensus: ConsensusEngineHandle<PayloadT>,
97        payload_store: PayloadStore<PayloadT>,
98        tx_pool: Pool,
99        task_spawner: Runtime,
100        client: ClientVersionV1,
101        capabilities: EngineCapabilities,
102        validator: Validator,
103        accept_execution_requests_hash: bool,
104        network: impl NetworkInfo + 'static,
105    ) -> Self {
106        let cell_custody = network.cell_custody().clone();
107        let is_syncing = Arc::new(move || network.is_syncing());
108        let inner = Arc::new(EngineApiInner {
109            provider,
110            chain_spec,
111            beacon_consensus,
112            payload_store,
113            task_spawner,
114            metrics: EngineApiMetrics::default(),
115            client,
116            capabilities,
117            tx_pool,
118            validator,
119            accept_execution_requests_hash,
120            cell_custody,
121            is_syncing,
122        });
123        Self { inner }
124    }
125
126    /// Fetches the client version.
127    pub fn get_client_version_v1(
128        &self,
129        _client: ClientVersionV1,
130    ) -> EngineApiResult<Vec<ClientVersionV1>> {
131        Ok(vec![self.inner.client.clone()])
132    }
133
134    /// Fetches the timestamp of the payload with the given id.
135    async fn get_payload_timestamp(&self, payload_id: PayloadId) -> EngineApiResult<u64> {
136        Ok(self
137            .inner
138            .payload_store
139            .payload_timestamp(payload_id)
140            .await
141            .ok_or(EngineApiError::UnknownPayload)??)
142    }
143
144    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_newpayloadv1>
145    /// Caution: This should not accept the `withdrawals` field
146    pub async fn new_payload_v1(
147        &self,
148        payload: PayloadT::ExecutionData,
149    ) -> EngineApiResult<PayloadStatus> {
150        let payload_or_attrs = PayloadOrAttributes::<
151            '_,
152            PayloadT::ExecutionData,
153            PayloadT::PayloadAttributes,
154        >::from_execution_payload(&payload);
155
156        self.inner
157            .validator
158            .validate_version_specific_fields(EngineApiMessageVersion::V1, payload_or_attrs)?;
159
160        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
161    }
162
163    /// Metered version of `new_payload_v1`.
164    pub async fn new_payload_v1_metered(
165        &self,
166        payload: PayloadT::ExecutionData,
167    ) -> EngineApiResult<PayloadStatus> {
168        let start = Instant::now();
169        let res = Self::new_payload_v1(self, payload).await;
170        let elapsed = start.elapsed();
171        self.inner.metrics.latency.new_payload_v1.record(elapsed);
172        res
173    }
174
175    /// See also <https://github.com/ethereum/execution-apis/blob/584905270d8ad665718058060267061ecfd79ca5/src/engine/shanghai.md#engine_newpayloadv2>
176    pub async fn new_payload_v2(
177        &self,
178        payload: PayloadT::ExecutionData,
179    ) -> EngineApiResult<PayloadStatus> {
180        let payload_or_attrs = PayloadOrAttributes::<
181            '_,
182            PayloadT::ExecutionData,
183            PayloadT::PayloadAttributes,
184        >::from_execution_payload(&payload);
185        self.inner
186            .validator
187            .validate_version_specific_fields(EngineApiMessageVersion::V2, payload_or_attrs)?;
188        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
189    }
190
191    /// Metered version of `new_payload_v2`.
192    pub async fn new_payload_v2_metered(
193        &self,
194        payload: PayloadT::ExecutionData,
195    ) -> EngineApiResult<PayloadStatus> {
196        let start = Instant::now();
197        let res = Self::new_payload_v2(self, payload).await;
198        let elapsed = start.elapsed();
199        self.inner.metrics.latency.new_payload_v2.record(elapsed);
200        res
201    }
202
203    /// See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_newpayloadv3>
204    pub async fn new_payload_v3(
205        &self,
206        payload: PayloadT::ExecutionData,
207    ) -> EngineApiResult<PayloadStatus> {
208        let payload_or_attrs = PayloadOrAttributes::<
209            '_,
210            PayloadT::ExecutionData,
211            PayloadT::PayloadAttributes,
212        >::from_execution_payload(&payload);
213        self.inner
214            .validator
215            .validate_version_specific_fields(EngineApiMessageVersion::V3, payload_or_attrs)?;
216
217        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
218    }
219
220    /// Metrics version of `new_payload_v3`
221    pub async fn new_payload_v3_metered(
222        &self,
223        payload: PayloadT::ExecutionData,
224    ) -> RpcResult<PayloadStatus> {
225        let start = Instant::now();
226
227        let res = Self::new_payload_v3(self, payload).await;
228        let elapsed = start.elapsed();
229        self.inner.metrics.latency.new_payload_v3.record(elapsed);
230        Ok(res?)
231    }
232
233    /// See also <https://github.com/ethereum/execution-apis/blob/7907424db935b93c2fe6a3c0faab943adebe8557/src/engine/prague.md#engine_newpayloadv4>
234    pub async fn new_payload_v4(
235        &self,
236        payload: PayloadT::ExecutionData,
237    ) -> EngineApiResult<PayloadStatus> {
238        let payload_or_attrs = PayloadOrAttributes::<
239            '_,
240            PayloadT::ExecutionData,
241            PayloadT::PayloadAttributes,
242        >::from_execution_payload(&payload);
243        self.inner
244            .validator
245            .validate_version_specific_fields(EngineApiMessageVersion::V4, payload_or_attrs)?;
246
247        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
248    }
249
250    /// Metrics version of `new_payload_v4`
251    pub async fn new_payload_v4_metered(
252        &self,
253        payload: PayloadT::ExecutionData,
254    ) -> RpcResult<PayloadStatus> {
255        let start = Instant::now();
256        let res = Self::new_payload_v4(self, payload).await;
257
258        let elapsed = start.elapsed();
259        self.inner.metrics.latency.new_payload_v4.record(elapsed);
260        Ok(res?)
261    }
262
263    /// Handler for `engine_newPayloadV5`
264    ///
265    /// Post-Amsterdam payload handler.
266    ///
267    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_newpayloadv5>
268    pub async fn new_payload_v5(
269        &self,
270        payload: PayloadT::ExecutionData,
271    ) -> EngineApiResult<PayloadStatus> {
272        let payload_or_attrs = PayloadOrAttributes::<
273            '_,
274            PayloadT::ExecutionData,
275            PayloadT::PayloadAttributes,
276        >::from_execution_payload(&payload);
277        self.inner
278            .validator
279            .validate_version_specific_fields(EngineApiMessageVersion::V5, payload_or_attrs)?;
280        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
281    }
282
283    /// Metrics version of `new_payload_v5`
284    pub async fn new_payload_v5_metered(
285        &self,
286        payload: PayloadT::ExecutionData,
287    ) -> RpcResult<PayloadStatus> {
288        let start = Instant::now();
289        let res = Self::new_payload_v5(self, payload).await;
290        let elapsed = start.elapsed();
291        self.inner.metrics.latency.new_payload_v5.record(elapsed);
292        Ok(res?)
293    }
294
295    /// Returns whether the engine accepts execution requests hash.
296    pub fn accept_execution_requests_hash(&self) -> bool {
297        self.inner.accept_execution_requests_hash
298    }
299}
300
301impl<Provider, EngineT, Pool, Validator, ChainSpec>
302    EngineApi<Provider, EngineT, Pool, Validator, ChainSpec>
303where
304    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
305    EngineT: EngineTypes,
306    Pool: TransactionPool + 'static,
307    Validator: EngineApiValidator<EngineT>,
308    ChainSpec: EthereumHardforks + Send + Sync + 'static,
309{
310    /// Sends a message to the beacon consensus engine to update the fork choice _without_
311    /// withdrawals.
312    ///
313    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_forkchoiceUpdatedV1>
314    ///
315    /// Caution: This should not accept the `withdrawals` field
316    pub async fn fork_choice_updated_v1(
317        &self,
318        state: ForkchoiceState,
319        payload_attrs: Option<EngineT::PayloadAttributes>,
320    ) -> EngineApiResult<ForkchoiceUpdated> {
321        self.validate_and_execute_forkchoice(EngineApiMessageVersion::V1, state, payload_attrs)
322            .await
323    }
324
325    /// Metrics version of `fork_choice_updated_v1`
326    pub async fn fork_choice_updated_v1_metered(
327        &self,
328        state: ForkchoiceState,
329        payload_attrs: Option<EngineT::PayloadAttributes>,
330    ) -> EngineApiResult<ForkchoiceUpdated> {
331        let start = Instant::now();
332        let res = Self::fork_choice_updated_v1(self, state, payload_attrs).await;
333        self.inner.metrics.latency.fork_choice_updated_v1.record(start.elapsed());
334        res
335    }
336
337    /// Sends a message to the beacon consensus engine to update the fork choice _with_ withdrawals,
338    /// but only _after_ shanghai.
339    ///
340    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/shanghai.md#engine_forkchoiceupdatedv2>
341    pub async fn fork_choice_updated_v2(
342        &self,
343        state: ForkchoiceState,
344        payload_attrs: Option<EngineT::PayloadAttributes>,
345    ) -> EngineApiResult<ForkchoiceUpdated> {
346        self.validate_and_execute_forkchoice(EngineApiMessageVersion::V2, state, payload_attrs)
347            .await
348    }
349
350    /// Metrics version of `fork_choice_updated_v2`
351    pub async fn fork_choice_updated_v2_metered(
352        &self,
353        state: ForkchoiceState,
354        payload_attrs: Option<EngineT::PayloadAttributes>,
355    ) -> EngineApiResult<ForkchoiceUpdated> {
356        let start = Instant::now();
357        let res = Self::fork_choice_updated_v2(self, state, payload_attrs).await;
358        self.inner.metrics.latency.fork_choice_updated_v2.record(start.elapsed());
359        res
360    }
361
362    /// Sends a message to the beacon consensus engine to update the fork choice _with_ withdrawals,
363    /// but only _after_ cancun.
364    ///
365    /// See also  <https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#engine_forkchoiceupdatedv3>
366    pub async fn fork_choice_updated_v3(
367        &self,
368        state: ForkchoiceState,
369        payload_attrs: Option<EngineT::PayloadAttributes>,
370    ) -> EngineApiResult<ForkchoiceUpdated> {
371        self.validate_and_execute_forkchoice(EngineApiMessageVersion::V3, state, payload_attrs)
372            .await
373    }
374
375    /// Metrics version of `fork_choice_updated_v3`
376    pub async fn fork_choice_updated_v3_metered(
377        &self,
378        state: ForkchoiceState,
379        payload_attrs: Option<EngineT::PayloadAttributes>,
380    ) -> EngineApiResult<ForkchoiceUpdated> {
381        let start = Instant::now();
382        let res = Self::fork_choice_updated_v3(self, state, payload_attrs).await;
383        self.inner.metrics.latency.fork_choice_updated_v3.record(start.elapsed());
384        res
385    }
386
387    /// Sends a message to the beacon consensus engine to update the fork choice _with_ slot number,
388    /// but only _after_ amsterdam.
389    ///
390    /// See also  <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_forkchoiceupdatedv4>
391    pub async fn fork_choice_updated_v4(
392        &self,
393        state: ForkchoiceState,
394        payload_attrs: Option<EngineT::PayloadAttributes>,
395        custody_columns: Option<B128>,
396    ) -> EngineApiResult<ForkchoiceUpdated> {
397        if let Some(custody_columns) = custody_columns {
398            self.inner.cell_custody.set(custody_columns);
399        }
400        self.validate_and_execute_forkchoice(EngineApiMessageVersion::V4, state, payload_attrs)
401            .await
402    }
403
404    /// Metrics version of `fork_choice_updated_v4`
405    pub async fn fork_choice_updated_v4_metered(
406        &self,
407        state: ForkchoiceState,
408        payload_attrs: Option<EngineT::PayloadAttributes>,
409        custody_columns: Option<B128>,
410    ) -> EngineApiResult<ForkchoiceUpdated> {
411        let start = Instant::now();
412        let res = Self::fork_choice_updated_v4(self, state, payload_attrs, custody_columns).await;
413        self.inner.metrics.latency.fork_choice_updated_v4.record(start.elapsed());
414        res
415    }
416
417    /// Helper function for retrieving the build payload by id.
418    async fn get_built_payload(
419        &self,
420        payload_id: PayloadId,
421    ) -> EngineApiResult<EngineT::BuiltPayload> {
422        self.inner
423            .payload_store
424            .resolve(payload_id)
425            .await
426            .ok_or(EngineApiError::UnknownPayload)?
427            .map_err(|_| EngineApiError::UnknownPayload)
428    }
429
430    /// Helper function for validating the payload timestamp and retrieving & converting the payload
431    /// into desired envelope.
432    async fn get_payload_inner<R>(
433        &self,
434        payload_id: PayloadId,
435        version: EngineApiMessageVersion,
436    ) -> EngineApiResult<R>
437    where
438        EngineT::BuiltPayload: TryInto<R>,
439    {
440        // Validate timestamp according to engine rules
441        // Enforces Osaka restrictions on `getPayloadV4`.
442        let timestamp = self.get_payload_timestamp(payload_id).await?;
443        validate_payload_timestamp(
444            &self.inner.chain_spec,
445            version,
446            timestamp,
447            MessageValidationKind::GetPayload,
448        )?;
449
450        // Now resolve the payload
451        self.get_built_payload(payload_id).await?.try_into().map_err(|_| {
452            warn!(?version, "could not transform built payload");
453            EngineApiError::UnknownPayload
454        })
455    }
456
457    /// Returns the most recent version of the payload that is available in the corresponding
458    /// payload build process at the time of receiving this call.
459    ///
460    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_getPayloadV1>
461    ///
462    /// Caution: This should not return the `withdrawals` field
463    ///
464    /// Note:
465    /// > Provider software MAY stop the corresponding build process after serving this call.
466    pub async fn get_payload_v1(
467        &self,
468        payload_id: PayloadId,
469    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV1> {
470        self.get_built_payload(payload_id).await?.try_into().map_err(|_| {
471            warn!(version = ?EngineApiMessageVersion::V1, "could not transform built payload");
472            EngineApiError::UnknownPayload
473        })
474    }
475
476    /// Metrics version of `get_payload_v1`
477    pub async fn get_payload_v1_metered(
478        &self,
479        payload_id: PayloadId,
480    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV1> {
481        let start = Instant::now();
482        let res = Self::get_payload_v1(self, payload_id).await;
483        self.inner.metrics.latency.get_payload_v1.record(start.elapsed());
484        res
485    }
486
487    /// Returns the most recent version of the payload that is available in the corresponding
488    /// payload build process at the time of receiving this call.
489    ///
490    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/shanghai.md#engine_getpayloadv2>
491    ///
492    /// Note:
493    /// > Provider software MAY stop the corresponding build process after serving this call.
494    pub async fn get_payload_v2(
495        &self,
496        payload_id: PayloadId,
497    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV2> {
498        self.get_payload_inner(payload_id, EngineApiMessageVersion::V2).await
499    }
500
501    /// Metrics version of `get_payload_v2`
502    pub async fn get_payload_v2_metered(
503        &self,
504        payload_id: PayloadId,
505    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV2> {
506        let start = Instant::now();
507        let res = Self::get_payload_v2(self, payload_id).await;
508        self.inner.metrics.latency.get_payload_v2.record(start.elapsed());
509        res
510    }
511
512    /// Returns the most recent version of the payload that is available in the corresponding
513    /// payload build process at the time of receiving this call.
514    ///
515    /// See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_getpayloadv3>
516    ///
517    /// Note:
518    /// > Provider software MAY stop the corresponding build process after serving this call.
519    pub async fn get_payload_v3(
520        &self,
521        payload_id: PayloadId,
522    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV3> {
523        self.get_payload_inner(payload_id, EngineApiMessageVersion::V3).await
524    }
525
526    /// Metrics version of `get_payload_v3`
527    pub async fn get_payload_v3_metered(
528        &self,
529        payload_id: PayloadId,
530    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV3> {
531        let start = Instant::now();
532        let res = Self::get_payload_v3(self, payload_id).await;
533        self.inner.metrics.latency.get_payload_v3.record(start.elapsed());
534        res
535    }
536
537    /// Returns the most recent version of the payload that is available in the corresponding
538    /// payload build process at the time of receiving this call.
539    ///
540    /// See also <https://github.com/ethereum/execution-apis/blob/7907424db935b93c2fe6a3c0faab943adebe8557/src/engine/prague.md#engine_getpayloadv4>
541    ///
542    /// Note:
543    /// > Provider software MAY stop the corresponding build process after serving this call.
544    pub async fn get_payload_v4(
545        &self,
546        payload_id: PayloadId,
547    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV4> {
548        self.get_payload_inner(payload_id, EngineApiMessageVersion::V4).await
549    }
550
551    /// Metrics version of `get_payload_v4`
552    pub async fn get_payload_v4_metered(
553        &self,
554        payload_id: PayloadId,
555    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV4> {
556        let start = Instant::now();
557        let res = Self::get_payload_v4(self, payload_id).await;
558        self.inner.metrics.latency.get_payload_v4.record(start.elapsed());
559        res
560    }
561
562    /// Handler for `engine_getPayloadV5`
563    ///
564    /// Returns the most recent version of the payload that is available in the corresponding
565    /// payload build process at the time of receiving this call.
566    ///
567    /// See also <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#engine_getpayloadv5>
568    ///
569    /// Note:
570    /// > Provider software MAY stop the corresponding build process after serving this call.
571    pub async fn get_payload_v5(
572        &self,
573        payload_id: PayloadId,
574    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV5> {
575        self.get_payload_inner(payload_id, EngineApiMessageVersion::V5).await
576    }
577
578    /// Metrics version of `get_payload_v5`
579    pub async fn get_payload_v5_metered(
580        &self,
581        payload_id: PayloadId,
582    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV5> {
583        let start = Instant::now();
584        let res = Self::get_payload_v5(self, payload_id).await;
585        self.inner.metrics.latency.get_payload_v5.record(start.elapsed());
586        res
587    }
588
589    /// Handler for `engine_getPayloadV6`
590    ///
591    /// Post-Amsterdam payload handler that includes Block Access Lists (BAL).
592    ///
593    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_getpayloadv6>
594    pub async fn get_payload_v6(
595        &self,
596        payload_id: PayloadId,
597    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV6> {
598        self.get_payload_inner(payload_id, EngineApiMessageVersion::V6).await
599    }
600
601    /// Metrics version of `get_payload_v6`
602    pub async fn get_payload_v6_metered(
603        &self,
604        payload_id: PayloadId,
605    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV6> {
606        let start = Instant::now();
607        let res = Self::get_payload_v6(self, payload_id).await;
608        self.inner.metrics.latency.get_payload_v6.record(start.elapsed());
609        res
610    }
611
612    /// Fetches all the blocks for the provided range starting at `start`, containing `count`
613    /// blocks and returns the mapped payload bodies.
614    pub async fn get_payload_bodies_by_range_with<F, R>(
615        &self,
616        start: BlockNumber,
617        count: u64,
618        f: F,
619    ) -> EngineApiResult<Vec<Option<R>>>
620    where
621        F: Fn(Provider::Block) -> R + Send + 'static,
622        R: Send + 'static,
623    {
624        let (tx, rx) = oneshot::channel();
625        let inner = self.inner.clone();
626
627        self.inner.task_spawner.spawn_blocking_task(async move {
628            if count > MAX_PAYLOAD_BODIES_LIMIT {
629                tx.send(Err(EngineApiError::PayloadRequestTooLarge { len: count })).ok();
630                return;
631            }
632
633            if start == 0 || count == 0 {
634                tx.send(Err(EngineApiError::InvalidBodiesRange { start, count })).ok();
635                return;
636            }
637
638            let mut result = Vec::with_capacity(count as usize);
639
640            // -1 so range is inclusive
641            let mut end = start.saturating_add(count - 1);
642
643            // > Client software MUST NOT return trailing null values if the request extends past the current latest known block.
644            // truncate the end if it's greater than the last block
645            if let Ok(best_block) = inner.provider.best_block_number()
646                && end > best_block {
647                    end = best_block;
648                }
649
650            // Check if the requested range starts before the earliest available block due to pruning/expiry
651            let earliest_block = inner.provider.earliest_block_number().unwrap_or(0);
652            for num in start..=end {
653                if num < earliest_block {
654                    result.push(None);
655                    continue;
656                }
657                let block_result = inner.provider.block(BlockHashOrNumber::Number(num));
658                match block_result {
659                    Ok(block) => {
660                        result.push(block.map(&f));
661                    }
662                    Err(err) => {
663                        tx.send(Err(EngineApiError::Internal(Box::new(err)))).ok();
664                        return;
665                    }
666                };
667            }
668            tx.send(Ok(result)).ok();
669        });
670
671        rx.await.map_err(|err| EngineApiError::Internal(Box::new(err)))?
672    }
673
674    /// Returns the execution payload bodies by the range starting at `start`, containing `count`
675    /// blocks.
676    ///
677    /// WARNING: This method is associated with the `BeaconBlocksByRange` message in the consensus
678    /// layer p2p specification, meaning the input should be treated as untrusted or potentially
679    /// adversarial.
680    ///
681    /// Implementers should take care when acting on the input to this method, specifically
682    /// ensuring that the range is limited properly, and that the range boundaries are computed
683    /// correctly and without panics.
684    pub async fn get_payload_bodies_by_range_v1(
685        &self,
686        start: BlockNumber,
687        count: u64,
688    ) -> EngineApiResult<ExecutionPayloadBodiesV1> {
689        self.get_payload_bodies_by_range_with(start, count, |block| ExecutionPayloadBodyV1 {
690            transactions: block.body().encoded_2718_transactions(),
691            withdrawals: block.body().withdrawals().cloned().map(Withdrawals::into_inner),
692        })
693        .await
694    }
695
696    /// Metrics version of `get_payload_bodies_by_range_v1`
697    pub async fn get_payload_bodies_by_range_v1_metered(
698        &self,
699        start: BlockNumber,
700        count: u64,
701    ) -> EngineApiResult<ExecutionPayloadBodiesV1> {
702        let start_time = Instant::now();
703        let res = Self::get_payload_bodies_by_range_v1(self, start, count).await;
704        self.inner.metrics.latency.get_payload_bodies_by_range_v1.record(start_time.elapsed());
705        res
706    }
707
708    /// Returns the execution payload bodies by the range (V2).
709    ///
710    /// V2 includes the `block_access_list` field for EIP-7928 BAL support.
711    pub async fn get_payload_bodies_by_range_v2(
712        &self,
713        start: BlockNumber,
714        count: u64,
715    ) -> EngineApiResult<ExecutionPayloadBodiesV2> {
716        let mut payload_bodies = self
717            .get_payload_bodies_by_range_with(start, count, |block| {
718                let block_hash = block.header().hash_slow();
719                (
720                    block_hash,
721                    ExecutionPayloadBodyV2 {
722                        transactions: block.body().encoded_2718_transactions(),
723                        withdrawals: block
724                            .body()
725                            .withdrawals()
726                            .cloned()
727                            .map(Withdrawals::into_inner),
728                        block_access_list: None,
729                    },
730                )
731            })
732            .await?;
733
734        let block_hashes = payload_bodies
735            .iter()
736            .filter_map(|payload_body| payload_body.as_ref().map(|(block_hash, _)| *block_hash))
737            .collect::<Vec<_>>();
738        let block_access_lists = self.get_block_access_lists_by_hashes(block_hashes).await?;
739
740        for (payload_body, block_access_list) in
741            payload_bodies.iter_mut().filter_map(Option::as_mut).zip(block_access_lists)
742        {
743            payload_body.1.block_access_list = block_access_list;
744        }
745
746        Ok(payload_bodies
747            .into_iter()
748            .map(|payload_body| payload_body.map(|(_, payload_body)| payload_body))
749            .collect())
750    }
751
752    /// Metrics version of `get_payload_bodies_by_range_v2`
753    pub async fn get_payload_bodies_by_range_v2_metered(
754        &self,
755        start: BlockNumber,
756        count: u64,
757    ) -> EngineApiResult<ExecutionPayloadBodiesV2> {
758        let start_time = Instant::now();
759        let res = Self::get_payload_bodies_by_range_v2(self, start, count).await;
760        self.inner.metrics.latency.get_payload_bodies_by_range_v2.record(start_time.elapsed());
761        res
762    }
763
764    /// Called to retrieve execution payload bodies by hashes.
765    pub async fn get_payload_bodies_by_hash_with<F, R>(
766        &self,
767        hashes: Vec<BlockHash>,
768        f: F,
769    ) -> EngineApiResult<Vec<Option<R>>>
770    where
771        F: Fn(Provider::Block) -> R + Send + 'static,
772        R: Send + 'static,
773    {
774        let len = hashes.len() as u64;
775        if len > MAX_PAYLOAD_BODIES_LIMIT {
776            return Err(EngineApiError::PayloadRequestTooLarge { len });
777        }
778
779        let (tx, rx) = oneshot::channel();
780        let inner = self.inner.clone();
781
782        self.inner.task_spawner.spawn_blocking_task(async move {
783            let mut result = Vec::with_capacity(hashes.len());
784            for hash in hashes {
785                let block_result = inner.provider.block(BlockHashOrNumber::Hash(hash));
786                match block_result {
787                    Ok(block) => {
788                        result.push(block.map(&f));
789                    }
790                    Err(err) => {
791                        let _ = tx.send(Err(EngineApiError::Internal(Box::new(err))));
792                        return;
793                    }
794                }
795            }
796            tx.send(Ok(result)).ok();
797        });
798
799        rx.await.map_err(|err| EngineApiError::Internal(Box::new(err)))?
800    }
801
802    async fn get_block_access_lists_by_hashes(
803        &self,
804        hashes: Vec<BlockHash>,
805    ) -> EngineApiResult<Vec<Option<Bytes>>> {
806        let len = hashes.len() as u64;
807        if len > MAX_PAYLOAD_BODIES_LIMIT {
808            return Err(EngineApiError::PayloadRequestTooLarge { len });
809        }
810
811        let (tx, rx) = oneshot::channel();
812        let bal_store = self.inner.provider.bal_store().clone();
813
814        self.inner.task_spawner.spawn_blocking_task(async move {
815            tx.send(
816                bal_store
817                    .get_by_hashes(&hashes)
818                    .map_err(|err| EngineApiError::Internal(Box::new(err))),
819            )
820            .ok();
821        });
822
823        rx.await.map_err(|err| EngineApiError::Internal(Box::new(err)))?
824    }
825
826    /// Called to retrieve execution payload bodies by hashes.
827    pub async fn get_payload_bodies_by_hash_v1(
828        &self,
829        hashes: Vec<BlockHash>,
830    ) -> EngineApiResult<ExecutionPayloadBodiesV1> {
831        self.get_payload_bodies_by_hash_with(hashes, |block| ExecutionPayloadBodyV1 {
832            transactions: block.body().encoded_2718_transactions(),
833            withdrawals: block.body().withdrawals().cloned().map(Withdrawals::into_inner),
834        })
835        .await
836    }
837
838    /// Metrics version of `get_payload_bodies_by_hash_v1`
839    pub async fn get_payload_bodies_by_hash_v1_metered(
840        &self,
841        hashes: Vec<BlockHash>,
842    ) -> EngineApiResult<ExecutionPayloadBodiesV1> {
843        let start = Instant::now();
844        let res = Self::get_payload_bodies_by_hash_v1(self, hashes).await;
845        self.inner.metrics.latency.get_payload_bodies_by_hash_v1.record(start.elapsed());
846        res
847    }
848
849    /// Called to retrieve execution payload bodies by hashes (V2).
850    ///
851    /// V2 includes the `block_access_list` field for EIP-7928 BAL support.
852    pub async fn get_payload_bodies_by_hash_v2(
853        &self,
854        hashes: Vec<BlockHash>,
855    ) -> EngineApiResult<ExecutionPayloadBodiesV2> {
856        let payload_bodies =
857            self.get_payload_bodies_by_hash_with(hashes.clone(), |block| ExecutionPayloadBodyV2 {
858                transactions: block.body().encoded_2718_transactions(),
859                withdrawals: block.body().withdrawals().cloned().map(Withdrawals::into_inner),
860                block_access_list: None,
861            });
862        let block_access_lists = self.get_block_access_lists_by_hashes(hashes);
863        let (mut payload_bodies, block_access_lists) =
864            tokio::try_join!(payload_bodies, block_access_lists)?;
865
866        for (payload_body, block_access_list) in payload_bodies.iter_mut().zip(block_access_lists) {
867            if let Some(payload_body) = payload_body {
868                payload_body.block_access_list = block_access_list;
869            }
870        }
871
872        Ok(payload_bodies)
873    }
874
875    /// Metrics version of `get_payload_bodies_by_hash_v2`
876    pub async fn get_payload_bodies_by_hash_v2_metered(
877        &self,
878        hashes: Vec<BlockHash>,
879    ) -> EngineApiResult<ExecutionPayloadBodiesV2> {
880        let start = Instant::now();
881        let res = Self::get_payload_bodies_by_hash_v2(self, hashes).await;
882        self.inner.metrics.latency.get_payload_bodies_by_hash_v2.record(start.elapsed());
883        res
884    }
885
886    /// Validates the `engine_forkchoiceUpdated` payload attributes and executes the forkchoice
887    /// update.
888    ///
889    /// The payload attributes will be validated according to the engine API rules for the given
890    /// message version:
891    /// * If the version is [`EngineApiMessageVersion::V1`], then the payload attributes will be
892    ///   validated according to the Paris rules.
893    /// * If the version is [`EngineApiMessageVersion::V2`], then the payload attributes will be
894    ///   validated according to the Shanghai rules, as well as the validity changes from cancun:
895    ///   <https://github.com/ethereum/execution-apis/blob/584905270d8ad665718058060267061ecfd79ca5/src/engine/cancun.md#update-the-methods-of-previous-forks>
896    ///
897    /// * If the version above [`EngineApiMessageVersion::V3`], then the payload attributes will be
898    ///   validated according to the Cancun rules.
899    async fn validate_and_execute_forkchoice(
900        &self,
901        version: EngineApiMessageVersion,
902        state: ForkchoiceState,
903        payload_attrs: Option<EngineT::PayloadAttributes>,
904    ) -> EngineApiResult<ForkchoiceUpdated> {
905        if let Some(ref attrs) = payload_attrs {
906            let attr_validation_res =
907                self.inner.validator.ensure_well_formed_attributes(version, attrs);
908
909            // From the engine API spec:
910            //
911            // Client software MUST ensure that payloadAttributes.timestamp is greater than
912            // timestamp of a block referenced by forkchoiceState.headBlockHash. If this condition
913            // isn't held client software MUST respond with -38003: Invalid payload attributes and
914            // MUST NOT begin a payload build process. In such an event, the forkchoiceState
915            // update MUST NOT be rolled back.
916            //
917            // NOTE: This also applies to cancun/shanghai-specific payload attributes.
918            if let Err(err) = attr_validation_res {
919                let fcu_res = self.inner.beacon_consensus.fork_choice_updated(state, None).await?;
920                if fcu_res.is_invalid() || fcu_res.payload_status.is_syncing() {
921                    return Ok(fcu_res)
922                }
923                return Err(err.into())
924            }
925        }
926
927        Ok(self.inner.beacon_consensus.fork_choice_updated(state, payload_attrs).await?)
928    }
929
930    /// Returns reference to supported capabilities.
931    pub fn capabilities(&self) -> &EngineCapabilities {
932        &self.inner.capabilities
933    }
934
935    fn has_blobs(&self, versioned_hashes: Vec<B256>) -> EngineApiResult<Vec<bool>> {
936        if versioned_hashes.len() > MAX_BLOB_LIMIT {
937            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
938        }
939
940        self.inner
941            .tx_pool
942            .has_blobs_for_versioned_hashes(&versioned_hashes)
943            .map_err(|err| EngineApiError::Internal(Box::new(err)))
944    }
945
946    /// Metered version of `has_blobs`.
947    pub fn has_blobs_metered(&self, versioned_hashes: Vec<B256>) -> EngineApiResult<Vec<bool>> {
948        let start = Instant::now();
949        let res = Self::has_blobs(self, versioned_hashes);
950        self.inner.metrics.latency.has_blobs.record(start.elapsed());
951        res
952    }
953
954    fn get_blobs_v1(
955        &self,
956        versioned_hashes: Vec<B256>,
957    ) -> EngineApiResult<Vec<Option<BlobAndProofV1>>> {
958        // Only allow this method before Osaka fork
959        let current_timestamp =
960            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
961        if self.inner.chain_spec.is_osaka_active_at_timestamp(current_timestamp) {
962            return Err(EngineApiError::EngineObjectValidationError(
963                reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
964            ));
965        }
966
967        if versioned_hashes.len() > MAX_BLOB_LIMIT {
968            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
969        }
970
971        self.inner
972            .tx_pool
973            .get_blobs_for_versioned_hashes_v1(&versioned_hashes)
974            .map_err(|err| EngineApiError::Internal(Box::new(err)))
975    }
976
977    /// Metered version of `get_blobs_v1`.
978    pub fn get_blobs_v1_metered(
979        &self,
980        versioned_hashes: Vec<B256>,
981    ) -> EngineApiResult<Vec<Option<BlobAndProofV1>>> {
982        let hashes_len = versioned_hashes.len();
983        let start = Instant::now();
984        let res = Self::get_blobs_v1(self, versioned_hashes);
985        self.inner.metrics.latency.get_blobs_v1.record(start.elapsed());
986
987        if let Ok(blobs) = &res {
988            let blobs_found = blobs.iter().flatten().count();
989            let blobs_missed = hashes_len - blobs_found;
990
991            self.inner.metrics.blob_metrics.blob_count.increment(blobs_found as u64);
992            self.inner.metrics.blob_metrics.blob_misses.increment(blobs_missed as u64);
993        }
994
995        res
996    }
997
998    fn get_blobs_v2(
999        &self,
1000        versioned_hashes: Vec<B256>,
1001    ) -> EngineApiResult<Option<Vec<BlobAndProofV2>>> {
1002        // Check if Osaka fork is active
1003        let current_timestamp =
1004            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
1005        if !self.inner.chain_spec.is_osaka_active_at_timestamp(current_timestamp) {
1006            return Err(EngineApiError::EngineObjectValidationError(
1007                reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
1008            ));
1009        }
1010
1011        if versioned_hashes.len() > MAX_BLOB_LIMIT {
1012            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
1013        }
1014
1015        self.inner
1016            .tx_pool
1017            .get_blobs_for_versioned_hashes_v2(&versioned_hashes)
1018            .map_err(|err| EngineApiError::Internal(Box::new(err)))
1019    }
1020
1021    fn get_blobs_v3(
1022        &self,
1023        versioned_hashes: Vec<B256>,
1024    ) -> EngineApiResult<Option<Vec<Option<BlobAndProofV2>>>> {
1025        // Check if Osaka fork is active
1026        let current_timestamp =
1027            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
1028        if !self.inner.chain_spec.is_osaka_active_at_timestamp(current_timestamp) {
1029            return Err(EngineApiError::EngineObjectValidationError(
1030                reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
1031            ));
1032        }
1033
1034        if versioned_hashes.len() > MAX_BLOB_LIMIT {
1035            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
1036        }
1037
1038        // Spec requires returning `null` if syncing.
1039        if (*self.inner.is_syncing)() {
1040            return Ok(None)
1041        }
1042
1043        self.inner
1044            .tx_pool
1045            .get_blobs_for_versioned_hashes_v3(&versioned_hashes)
1046            .map(Some)
1047            .map_err(|err| EngineApiError::Internal(Box::new(err)))
1048    }
1049
1050    fn get_blobs_v4(
1051        &self,
1052        versioned_hashes: Vec<B256>,
1053        indices_bitarray: B128,
1054    ) -> EngineApiResult<Option<Vec<Option<BlobCellsAndProofsV1>>>> {
1055        let current_timestamp =
1056            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
1057        if !self.inner.chain_spec.is_amsterdam_active_at_timestamp(current_timestamp) {
1058            return Err(EngineApiError::EngineObjectValidationError(
1059                reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
1060            ));
1061        }
1062
1063        if versioned_hashes.len() > MAX_BLOB_LIMIT {
1064            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
1065        }
1066
1067        // Spec requires returning `null` if syncing.
1068        if (*self.inner.is_syncing)() {
1069            return Ok(None)
1070        }
1071
1072        self.inner
1073            .tx_pool
1074            .get_blobs_for_versioned_hashes_v4(&versioned_hashes, indices_bitarray)
1075            .map(Some)
1076            .map_err(|err| EngineApiError::Internal(Box::new(err)))
1077    }
1078
1079    /// Metered version of `get_blobs_v2`.
1080    pub fn get_blobs_v2_metered(
1081        &self,
1082        versioned_hashes: Vec<B256>,
1083    ) -> EngineApiResult<Option<Vec<BlobAndProofV2>>> {
1084        let hashes_len = versioned_hashes.len();
1085        let start = Instant::now();
1086        let res = Self::get_blobs_v2(self, versioned_hashes);
1087        self.inner.metrics.latency.get_blobs_v2.record(start.elapsed());
1088
1089        if let Ok(blobs) = &res {
1090            let blobs_found = blobs.iter().flatten().count();
1091
1092            self.inner
1093                .metrics
1094                .blob_metrics
1095                .get_blobs_requests_blobs_total
1096                .increment(hashes_len as u64);
1097            self.inner
1098                .metrics
1099                .blob_metrics
1100                .get_blobs_requests_blobs_in_blobpool_total
1101                .increment(blobs_found as u64);
1102
1103            if blobs_found == hashes_len {
1104                self.inner.metrics.blob_metrics.get_blobs_requests_success_total.increment(1);
1105            } else {
1106                self.inner.metrics.blob_metrics.get_blobs_requests_failure_total.increment(1);
1107            }
1108        } else {
1109            self.inner.metrics.blob_metrics.get_blobs_requests_failure_total.increment(1);
1110        }
1111
1112        res
1113    }
1114
1115    /// Metered version of `get_blobs_v3`.
1116    pub fn get_blobs_v3_metered(
1117        &self,
1118        versioned_hashes: Vec<B256>,
1119    ) -> EngineApiResult<Option<Vec<Option<BlobAndProofV2>>>> {
1120        let hashes_len = versioned_hashes.len();
1121        let start = Instant::now();
1122        let res = Self::get_blobs_v3(self, versioned_hashes);
1123        self.inner.metrics.latency.get_blobs_v3.record(start.elapsed());
1124
1125        if let Ok(Some(blobs)) = &res {
1126            let blobs_found = blobs.iter().flatten().count();
1127            let blobs_missed = hashes_len - blobs_found;
1128
1129            self.inner.metrics.blob_metrics.blob_count.increment(blobs_found as u64);
1130            self.inner.metrics.blob_metrics.blob_misses.increment(blobs_missed as u64);
1131        }
1132
1133        res
1134    }
1135
1136    /// Metered version of `get_blobs_v4`.
1137    pub fn get_blobs_v4_metered(
1138        &self,
1139        versioned_hashes: Vec<B256>,
1140        indices_bitarray: B128,
1141    ) -> EngineApiResult<Option<Vec<Option<BlobCellsAndProofsV1>>>> {
1142        let hashes_len = versioned_hashes.len();
1143        let start = Instant::now();
1144        let res = Self::get_blobs_v4(self, versioned_hashes, indices_bitarray);
1145        self.inner.metrics.latency.get_blobs_v4.record(start.elapsed());
1146
1147        if let Ok(Some(blobs)) = &res {
1148            let blobs_found = blobs.iter().flatten().count();
1149            let blobs_missed = hashes_len - blobs_found;
1150
1151            self.inner.metrics.blob_metrics.blob_count.increment(blobs_found as u64);
1152            self.inner.metrics.blob_metrics.blob_misses.increment(blobs_missed as u64);
1153        }
1154
1155        res
1156    }
1157}
1158
1159// This is the concrete ethereum engine API implementation.
1160#[async_trait]
1161impl<Provider, EngineT, Pool, Validator, ChainSpec> EngineApiServer<EngineT>
1162    for EngineApi<Provider, EngineT, Pool, Validator, ChainSpec>
1163where
1164    Provider: HeaderProvider + BlockReader + StateProviderFactory + BalProvider + 'static,
1165    EngineT: EngineTypes<ExecutionData = ExecutionData>,
1166    Pool: TransactionPool + 'static,
1167    Validator: EngineApiValidator<EngineT>,
1168    ChainSpec: EthereumHardforks + Send + Sync + 'static,
1169{
1170    /// Handler for `engine_newPayloadV1`
1171    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_newpayloadv1>
1172    /// Caution: This should not accept the `withdrawals` field
1173    async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> RpcResult<PayloadStatus> {
1174        trace!(target: "rpc::engine", "Serving engine_newPayloadV1");
1175        let payload =
1176            ExecutionData { payload: payload.into(), sidecar: ExecutionPayloadSidecar::none() };
1177        Ok(self.new_payload_v1_metered(payload).await?)
1178    }
1179
1180    /// Handler for `engine_newPayloadV2`
1181    /// See also <https://github.com/ethereum/execution-apis/blob/584905270d8ad665718058060267061ecfd79ca5/src/engine/shanghai.md#engine_newpayloadv2>
1182    async fn new_payload_v2(&self, payload: ExecutionPayloadInputV2) -> RpcResult<PayloadStatus> {
1183        trace!(target: "rpc::engine", "Serving engine_newPayloadV2");
1184        let payload = ExecutionData {
1185            payload: payload.into_payload(),
1186            sidecar: ExecutionPayloadSidecar::none(),
1187        };
1188
1189        Ok(self.new_payload_v2_metered(payload).await?)
1190    }
1191
1192    /// Handler for `engine_newPayloadV3`
1193    /// See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_newpayloadv3>
1194    async fn new_payload_v3(
1195        &self,
1196        payload: ExecutionPayloadV3,
1197        versioned_hashes: Vec<B256>,
1198        parent_beacon_block_root: B256,
1199    ) -> RpcResult<PayloadStatus> {
1200        trace!(target: "rpc::engine", "Serving engine_newPayloadV3");
1201        let payload = ExecutionData {
1202            payload: payload.into(),
1203            sidecar: ExecutionPayloadSidecar::v3(CancunPayloadFields {
1204                versioned_hashes,
1205                parent_beacon_block_root,
1206            }),
1207        };
1208
1209        Ok(self.new_payload_v3_metered(payload).await?)
1210    }
1211
1212    /// Handler for `engine_newPayloadV4`
1213    /// See also <https://github.com/ethereum/execution-apis/blob/03911ffc053b8b806123f1fc237184b0092a485a/src/engine/prague.md#engine_newpayloadv4>
1214    async fn new_payload_v4(
1215        &self,
1216        payload: ExecutionPayloadV3,
1217        versioned_hashes: Vec<B256>,
1218        parent_beacon_block_root: B256,
1219        requests: RequestsOrHash,
1220    ) -> RpcResult<PayloadStatus> {
1221        trace!(target: "rpc::engine", "Serving engine_newPayloadV4");
1222
1223        // Accept requests as a hash only if it is explicitly allowed
1224        if requests.is_hash() && !self.inner.accept_execution_requests_hash {
1225            return Err(EngineApiError::UnexpectedRequestsHash.into());
1226        }
1227
1228        let payload = ExecutionData {
1229            payload: payload.into(),
1230            sidecar: ExecutionPayloadSidecar::v4(
1231                CancunPayloadFields { versioned_hashes, parent_beacon_block_root },
1232                PraguePayloadFields { requests },
1233            ),
1234        };
1235
1236        Ok(self.new_payload_v4_metered(payload).await?)
1237    }
1238
1239    /// Handler for `engine_newPayloadV5`
1240    ///
1241    /// Post Amsterdam payload handler.
1242    ///
1243    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_newpayloadv5>
1244    async fn new_payload_v5(
1245        &self,
1246        payload: ExecutionPayloadV4,
1247        versioned_hashes: Vec<B256>,
1248        parent_beacon_block_root: B256,
1249        requests: RequestsOrHash,
1250    ) -> RpcResult<PayloadStatus> {
1251        trace!(target: "rpc::engine", "Serving engine_newPayloadV5");
1252        // Accept requests as a hash only if it is explicitly allowed.
1253        if requests.is_hash() && !self.inner.accept_execution_requests_hash {
1254            return Err(EngineApiError::UnexpectedRequestsHash.into());
1255        }
1256
1257        let payload = ExecutionData {
1258            payload: payload.into(),
1259            sidecar: ExecutionPayloadSidecar::v4(
1260                CancunPayloadFields { versioned_hashes, parent_beacon_block_root },
1261                PraguePayloadFields { requests },
1262            ),
1263        };
1264
1265        Ok(self.new_payload_v5_metered(payload).await?)
1266    }
1267
1268    /// Handler for `engine_forkchoiceUpdatedV1`
1269    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_forkchoiceupdatedv1>
1270    ///
1271    /// Caution: This should not accept the `withdrawals` field
1272    async fn fork_choice_updated_v1(
1273        &self,
1274        fork_choice_state: ForkchoiceState,
1275        payload_attributes: Option<EngineT::PayloadAttributes>,
1276    ) -> RpcResult<ForkchoiceUpdated> {
1277        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV1");
1278        Ok(self.fork_choice_updated_v1_metered(fork_choice_state, payload_attributes).await?)
1279    }
1280
1281    /// Handler for `engine_forkchoiceUpdatedV2`
1282    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/shanghai.md#engine_forkchoiceupdatedv2>
1283    async fn fork_choice_updated_v2(
1284        &self,
1285        fork_choice_state: ForkchoiceState,
1286        payload_attributes: Option<EngineT::PayloadAttributes>,
1287    ) -> RpcResult<ForkchoiceUpdated> {
1288        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV2");
1289        Ok(self.fork_choice_updated_v2_metered(fork_choice_state, payload_attributes).await?)
1290    }
1291
1292    /// Handler for `engine_forkchoiceUpdatedV3`
1293    ///
1294    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#engine_forkchoiceupdatedv3>
1295    async fn fork_choice_updated_v3(
1296        &self,
1297        fork_choice_state: ForkchoiceState,
1298        payload_attributes: Option<EngineT::PayloadAttributes>,
1299    ) -> RpcResult<ForkchoiceUpdated> {
1300        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV3");
1301        Ok(self.fork_choice_updated_v3_metered(fork_choice_state, payload_attributes).await?)
1302    }
1303
1304    /// Handler for `engine_forkchoiceUpdatedV4`
1305    ///
1306    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_forkchoiceupdatedv4>
1307    async fn fork_choice_updated_v4(
1308        &self,
1309        fork_choice_state: ForkchoiceState,
1310        payload_attributes: Option<EngineT::PayloadAttributes>,
1311        custody_columns: Option<B128>,
1312    ) -> RpcResult<ForkchoiceUpdated> {
1313        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV4");
1314        Ok(self
1315            .fork_choice_updated_v4_metered(fork_choice_state, payload_attributes, custody_columns)
1316            .await?)
1317    }
1318
1319    /// Handler for `engine_getPayloadV1`
1320    ///
1321    /// Returns the most recent version of the payload that is available in the corresponding
1322    /// payload build process at the time of receiving this call.
1323    ///
1324    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_getPayloadV1>
1325    ///
1326    /// Caution: This should not return the `withdrawals` field
1327    ///
1328    /// Note:
1329    /// > Provider software MAY stop the corresponding build process after serving this call.
1330    async fn get_payload_v1(
1331        &self,
1332        payload_id: PayloadId,
1333    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV1> {
1334        trace!(target: "rpc::engine", "Serving engine_getPayloadV1");
1335        Ok(self.get_payload_v1_metered(payload_id).await?)
1336    }
1337
1338    /// Handler for `engine_getPayloadV2`
1339    ///
1340    /// Returns the most recent version of the payload that is available in the corresponding
1341    /// payload build process at the time of receiving this call.
1342    ///
1343    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/shanghai.md#engine_getpayloadv2>
1344    ///
1345    /// Note:
1346    /// > Provider software MAY stop the corresponding build process after serving this call.
1347    async fn get_payload_v2(
1348        &self,
1349        payload_id: PayloadId,
1350    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV2> {
1351        debug!(target: "rpc::engine", id = %payload_id, "Serving engine_getPayloadV2");
1352        Ok(self.get_payload_v2_metered(payload_id).await?)
1353    }
1354
1355    /// Handler for `engine_getPayloadV3`
1356    ///
1357    /// Returns the most recent version of the payload that is available in the corresponding
1358    /// payload build process at the time of receiving this call.
1359    ///
1360    /// See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_getpayloadv3>
1361    ///
1362    /// Note:
1363    /// > Provider software MAY stop the corresponding build process after serving this call.
1364    async fn get_payload_v3(
1365        &self,
1366        payload_id: PayloadId,
1367    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV3> {
1368        trace!(target: "rpc::engine", "Serving engine_getPayloadV3");
1369        Ok(self.get_payload_v3_metered(payload_id).await?)
1370    }
1371
1372    /// Handler for `engine_getPayloadV4`
1373    ///
1374    /// Returns the most recent version of the payload that is available in the corresponding
1375    /// payload build process at the time of receiving this call.
1376    ///
1377    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md#engine_getpayloadv4>
1378    ///
1379    /// Note:
1380    /// > Provider software MAY stop the corresponding build process after serving this call.
1381    async fn get_payload_v4(
1382        &self,
1383        payload_id: PayloadId,
1384    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV4> {
1385        trace!(target: "rpc::engine", "Serving engine_getPayloadV4");
1386        Ok(self.get_payload_v4_metered(payload_id).await?)
1387    }
1388
1389    /// Handler for `engine_getPayloadV5`
1390    ///
1391    /// Returns the most recent version of the payload that is available in the corresponding
1392    /// payload build process at the time of receiving this call.
1393    ///
1394    /// See also <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#engine_getpayloadv5>
1395    ///
1396    /// Note:
1397    /// > Provider software MAY stop the corresponding build process after serving this call.
1398    async fn get_payload_v5(
1399        &self,
1400        payload_id: PayloadId,
1401    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV5> {
1402        trace!(target: "rpc::engine", "Serving engine_getPayloadV5");
1403        Ok(self.get_payload_v5_metered(payload_id).await?)
1404    }
1405
1406    /// Handler for `engine_getPayloadV6`
1407    ///
1408    /// Post-Amsterdam payload handler that includes Block Access Lists (BAL).
1409    ///
1410    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_getpayloadv6>
1411    async fn get_payload_v6(
1412        &self,
1413        payload_id: PayloadId,
1414    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV6> {
1415        trace!(target: "rpc::engine", "Serving engine_getPayloadV6");
1416        Ok(self.get_payload_v6_metered(payload_id).await?)
1417    }
1418
1419    /// Handler for `engine_getPayloadBodiesByHashV1`
1420    /// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#engine_getpayloadbodiesbyhashv1>
1421    async fn get_payload_bodies_by_hash_v1(
1422        &self,
1423        block_hashes: Vec<BlockHash>,
1424    ) -> RpcResult<ExecutionPayloadBodiesV1> {
1425        trace!(target: "rpc::engine", "Serving engine_getPayloadBodiesByHashV1");
1426        Ok(self.get_payload_bodies_by_hash_v1_metered(block_hashes).await?)
1427    }
1428
1429    /// Handler for `engine_getPayloadBodiesByHashV2`
1430    ///
1431    /// V2 includes the `block_access_list` field for EIP-7928 BAL support.
1432    ///
1433    /// See also <https://eips.ethereum.org/EIPS/eip-7928>
1434    async fn get_payload_bodies_by_hash_v2(
1435        &self,
1436        block_hashes: Vec<BlockHash>,
1437    ) -> RpcResult<ExecutionPayloadBodiesV2> {
1438        trace!(target: "rpc::engine", "Serving engine_getPayloadBodiesByHashV2");
1439        Ok(self.get_payload_bodies_by_hash_v2_metered(block_hashes).await?)
1440    }
1441
1442    /// Handler for `engine_getPayloadBodiesByRangeV1`
1443    ///
1444    /// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#engine_getpayloadbodiesbyrangev1>
1445    ///
1446    /// Returns the execution payload bodies by the range starting at `start`, containing `count`
1447    /// blocks.
1448    ///
1449    /// WARNING: This method is associated with the `BeaconBlocksByRange` message in the consensus
1450    /// layer p2p specification, meaning the input should be treated as untrusted or potentially
1451    /// adversarial.
1452    ///
1453    /// Implementers should take care when acting on the input to this method, specifically
1454    /// ensuring that the range is limited properly, and that the range boundaries are computed
1455    /// correctly and without panics.
1456    ///
1457    /// Note: If a block is pre shanghai, `withdrawals` field will be `null`.
1458    async fn get_payload_bodies_by_range_v1(
1459        &self,
1460        start: U64,
1461        count: U64,
1462    ) -> RpcResult<ExecutionPayloadBodiesV1> {
1463        trace!(target: "rpc::engine", "Serving engine_getPayloadBodiesByRangeV1");
1464        Ok(self.get_payload_bodies_by_range_v1_metered(start.to(), count.to()).await?)
1465    }
1466
1467    /// Handler for `engine_getPayloadBodiesByRangeV2`
1468    ///
1469    /// V2 includes the `block_access_list` field for EIP-7928 BAL support.
1470    ///
1471    /// See also <https://eips.ethereum.org/EIPS/eip-7928>
1472    async fn get_payload_bodies_by_range_v2(
1473        &self,
1474        start: U64,
1475        count: U64,
1476    ) -> RpcResult<ExecutionPayloadBodiesV2> {
1477        trace!(target: "rpc::engine", "Serving engine_getPayloadBodiesByRangeV2");
1478        Ok(self.get_payload_bodies_by_range_v2_metered(start.to(), count.to()).await?)
1479    }
1480
1481    /// Handler for `engine_getClientVersionV1`
1482    ///
1483    /// See also <https://github.com/ethereum/execution-apis/blob/03911ffc053b8b806123f1fc237184b0092a485a/src/engine/identification.md>
1484    async fn get_client_version_v1(
1485        &self,
1486        client: ClientVersionV1,
1487    ) -> RpcResult<Vec<ClientVersionV1>> {
1488        trace!(target: "rpc::engine", "Serving engine_getClientVersionV1");
1489        Ok(Self::get_client_version_v1(self, client)?)
1490    }
1491
1492    /// Handler for `engine_exchangeCapabilitiesV1`
1493    /// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/common.md#capabilities>
1494    async fn exchange_capabilities(&self, capabilities: Vec<String>) -> RpcResult<Vec<String>> {
1495        trace!(target: "rpc::engine", "Serving engine_exchangeCapabilities");
1496
1497        let el_caps = self.capabilities();
1498        el_caps.log_capability_mismatches(&capabilities);
1499
1500        Ok(el_caps.list())
1501    }
1502
1503    async fn has_blobs(&self, versioned_hashes: Vec<B256>) -> RpcResult<Vec<bool>> {
1504        trace!(target: "rpc::engine", "Serving engine_hasBlobs");
1505        Ok(self.has_blobs_metered(versioned_hashes)?)
1506    }
1507
1508    async fn get_blobs_v1(
1509        &self,
1510        versioned_hashes: Vec<B256>,
1511    ) -> RpcResult<Vec<Option<BlobAndProofV1>>> {
1512        trace!(target: "rpc::engine", "Serving engine_getBlobsV1");
1513        Ok(self.get_blobs_v1_metered(versioned_hashes)?)
1514    }
1515
1516    async fn get_blobs_v2(
1517        &self,
1518        versioned_hashes: Vec<B256>,
1519    ) -> RpcResult<Option<Vec<BlobAndProofV2>>> {
1520        trace!(target: "rpc::engine", "Serving engine_getBlobsV2");
1521        Ok(self.get_blobs_v2_metered(versioned_hashes)?)
1522    }
1523
1524    async fn get_blobs_v3(
1525        &self,
1526        versioned_hashes: Vec<B256>,
1527    ) -> RpcResult<Option<Vec<Option<BlobAndProofV2>>>> {
1528        trace!(target: "rpc::engine", "Serving engine_getBlobsV3");
1529        Ok(self.get_blobs_v3_metered(versioned_hashes)?)
1530    }
1531
1532    async fn get_blobs_v4(
1533        &self,
1534        versioned_hashes: Vec<B256>,
1535        indices_bitarray: B128,
1536    ) -> RpcResult<Option<Vec<Option<BlobCellsAndProofsV1>>>> {
1537        trace!(target: "rpc::engine", "Serving engine_getBlobsV4");
1538        Ok(self.get_blobs_v4_metered(versioned_hashes, indices_bitarray)?)
1539    }
1540}
1541
1542impl<Provider, EngineT, Pool, Validator, ChainSpec> IntoEngineApiRpcModule
1543    for EngineApi<Provider, EngineT, Pool, Validator, ChainSpec>
1544where
1545    EngineT: EngineTypes,
1546    Self: EngineApiServer<EngineT>,
1547{
1548    fn into_rpc_module(self) -> RpcModule<()> {
1549        EngineApiServer::<EngineT>::into_rpc(self).remove_context()
1550    }
1551}
1552
1553impl<Provider, PayloadT, Pool, Validator, ChainSpec> std::fmt::Debug
1554    for EngineApi<Provider, PayloadT, Pool, Validator, ChainSpec>
1555where
1556    PayloadT: PayloadTypes,
1557{
1558    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1559        f.debug_struct("EngineApi").finish_non_exhaustive()
1560    }
1561}
1562
1563impl<Provider, PayloadT, Pool, Validator, ChainSpec> Clone
1564    for EngineApi<Provider, PayloadT, Pool, Validator, ChainSpec>
1565where
1566    PayloadT: PayloadTypes,
1567{
1568    fn clone(&self) -> Self {
1569        Self { inner: Arc::clone(&self.inner) }
1570    }
1571}
1572
1573/// The container type for the engine API internals.
1574struct EngineApiInner<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSpec> {
1575    /// The provider to interact with the chain.
1576    provider: Provider,
1577    /// Consensus configuration
1578    chain_spec: Arc<ChainSpec>,
1579    /// The channel to send messages to the beacon consensus engine.
1580    beacon_consensus: ConsensusEngineHandle<PayloadT>,
1581    /// The type that can communicate with the payload service to retrieve payloads.
1582    payload_store: PayloadStore<PayloadT>,
1583    /// For spawning and executing async tasks
1584    task_spawner: Runtime,
1585    /// The latency and response type metrics for engine api calls
1586    metrics: EngineApiMetrics,
1587    /// Identification of the execution client used by the consensus client
1588    client: ClientVersionV1,
1589    /// The list of all supported Engine capabilities available over the engine endpoint.
1590    capabilities: EngineCapabilities,
1591    /// Transaction pool.
1592    tx_pool: Pool,
1593    /// Engine validator.
1594    validator: Validator,
1595    accept_execution_requests_hash: bool,
1596    /// Shared blob cell custody bitmap.
1597    cell_custody: CellCustody,
1598    /// Returns `true` if the node is currently syncing.
1599    is_syncing: Arc<dyn Fn() -> bool + Send + Sync>,
1600}
1601
1602#[cfg(test)]
1603mod tests {
1604    use super::*;
1605    use alloy_eips::{eip7685::Requests, NumHash};
1606    use alloy_primitives::{Address, Bytes, B256};
1607    use alloy_rpc_types_engine::{
1608        ClientCode, ClientVersionV1, ExecutionPayloadV2, PayloadAttributes, PayloadStatusEnum,
1609    };
1610    use assert_matches::assert_matches;
1611    use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET};
1612    use reth_engine_primitives::{BeaconEngineMessage, OnForkChoiceUpdated};
1613    use reth_ethereum_engine_primitives::EthEngineTypes;
1614    use reth_ethereum_primitives::Block;
1615    use reth_network_api::{
1616        noop::NoopNetwork, EthProtocolInfo, NetworkError, NetworkInfo, NetworkStatus,
1617    };
1618    use reth_node_ethereum::EthereumEngineValidator;
1619    use reth_payload_builder::test_utils::spawn_test_payload_service;
1620    use reth_provider::{test_utils::MockEthProvider, BalStoreHandle, InMemoryBalStore, RawBal};
1621    use reth_tasks::Runtime;
1622    use reth_transaction_pool::noop::NoopTransactionPool;
1623    use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
1624
1625    fn setup_engine_api() -> (
1626        EngineApiTestHandle,
1627        EngineApi<
1628            Arc<MockEthProvider>,
1629            EthEngineTypes,
1630            NoopTransactionPool,
1631            EthereumEngineValidator,
1632            ChainSpec,
1633        >,
1634    ) {
1635        let client = ClientVersionV1 {
1636            code: ClientCode::RH,
1637            name: "Reth".to_string(),
1638            version: "v0.2.0-beta.5".to_string(),
1639            commit: "defa64b2".to_string(),
1640        };
1641
1642        let chain_spec: Arc<ChainSpec> = MAINNET.clone();
1643        let provider = Arc::new(MockEthProvider::default());
1644        let payload_store = spawn_test_payload_service();
1645        let (to_engine, engine_rx) = unbounded_channel();
1646        let task_executor = Runtime::test();
1647        let api = EngineApi::new(
1648            provider.clone(),
1649            chain_spec.clone(),
1650            ConsensusEngineHandle::new(to_engine),
1651            payload_store.into(),
1652            NoopTransactionPool::default(),
1653            task_executor,
1654            client,
1655            EngineCapabilities::default(),
1656            EthereumEngineValidator::new(chain_spec.clone()),
1657            false,
1658            NoopNetwork::default(),
1659        );
1660        let handle = EngineApiTestHandle { chain_spec, provider, from_api: engine_rx };
1661        (handle, api)
1662    }
1663
1664    #[tokio::test]
1665    async fn engine_client_version_v1() {
1666        let client = ClientVersionV1 {
1667            code: ClientCode::RH,
1668            name: "Reth".to_string(),
1669            version: "v0.2.0-beta.5".to_string(),
1670            commit: "defa64b2".to_string(),
1671        };
1672        let (_, api) = setup_engine_api();
1673        let res = api.get_client_version_v1(client.clone());
1674        assert_eq!(res.unwrap(), vec![client]);
1675    }
1676
1677    #[tokio::test]
1678    async fn has_blobs_returns_ordered_availability() {
1679        let (_, api) = setup_engine_api();
1680
1681        let res = api.has_blobs_metered(vec![B256::ZERO, B256::with_last_byte(1)]).unwrap();
1682        assert_eq!(res, vec![false, false]);
1683    }
1684
1685    #[tokio::test]
1686    async fn has_blobs_rejects_large_requests() {
1687        let (_, api) = setup_engine_api();
1688
1689        let res = api.has_blobs_metered(vec![B256::ZERO; MAX_BLOB_LIMIT + 1]);
1690        assert_matches!(
1691            res,
1692            Err(EngineApiError::BlobRequestTooLarge { len }) if len == MAX_BLOB_LIMIT + 1
1693        );
1694    }
1695
1696    #[tokio::test]
1697    async fn get_payload_bodies_by_hash_v2_returns_block_access_list_from_store() {
1698        let bal_store = BalStoreHandle::new(InMemoryBalStore::default());
1699        let mut provider = MockEthProvider::default();
1700        provider.bal_store = bal_store.clone();
1701        let provider = Arc::new(provider);
1702
1703        let client = ClientVersionV1 {
1704            code: ClientCode::RH,
1705            name: "Reth".to_string(),
1706            version: "v0.2.0-beta.5".to_string(),
1707            commit: "defa64b2".to_string(),
1708        };
1709        let chain_spec: Arc<ChainSpec> = MAINNET.clone();
1710        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
1711        let (to_engine, _engine_rx) = unbounded_channel();
1712        let api = EngineApi::new(
1713            provider.clone(),
1714            chain_spec.clone(),
1715            ConsensusEngineHandle::new(to_engine),
1716            payload_store.into(),
1717            NoopTransactionPool::default(),
1718            Runtime::test(),
1719            client,
1720            EngineCapabilities::default(),
1721            EthereumEngineValidator::new(chain_spec),
1722            false,
1723            NoopNetwork::default(),
1724        );
1725
1726        let mut block = Block::default();
1727        block.header.number = 1;
1728        let block_hash = block.header.hash_slow();
1729        provider.add_block(block_hash, block);
1730
1731        let mut block_without_bal = Block::default();
1732        block_without_bal.header.number = 2;
1733        let block_without_bal_hash = block_without_bal.header.hash_slow();
1734        provider.add_block(block_without_bal_hash, block_without_bal);
1735
1736        let raw_bal = Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]);
1737        bal_store.insert(NumHash::new(1, block_hash), RawBal::new(raw_bal.clone())).unwrap();
1738
1739        let missing_hash = B256::with_last_byte(3);
1740        let response = api
1741            .get_payload_bodies_by_hash_v2(vec![block_hash, block_without_bal_hash, missing_hash])
1742            .await
1743            .unwrap();
1744
1745        assert_eq!(response.len(), 3);
1746        assert_eq!(response[0].as_ref().unwrap().block_access_list, Some(raw_bal));
1747        assert_eq!(response[1].as_ref().unwrap().block_access_list, None);
1748        assert!(response[2].is_none());
1749    }
1750
1751    #[tokio::test]
1752    async fn get_payload_bodies_by_range_v2_returns_block_access_lists_from_store() {
1753        let bal_store = BalStoreHandle::new(InMemoryBalStore::default());
1754        let mut provider = MockEthProvider::default();
1755        provider.bal_store = bal_store.clone();
1756        let provider = Arc::new(provider);
1757
1758        let client = ClientVersionV1 {
1759            code: ClientCode::RH,
1760            name: "Reth".to_string(),
1761            version: "v0.2.0-beta.5".to_string(),
1762            commit: "defa64b2".to_string(),
1763        };
1764        let chain_spec: Arc<ChainSpec> = MAINNET.clone();
1765        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
1766        let (to_engine, _engine_rx) = unbounded_channel();
1767        let api = EngineApi::new(
1768            provider.clone(),
1769            chain_spec.clone(),
1770            ConsensusEngineHandle::new(to_engine),
1771            payload_store.into(),
1772            NoopTransactionPool::default(),
1773            Runtime::test(),
1774            client,
1775            EngineCapabilities::default(),
1776            EthereumEngineValidator::new(chain_spec),
1777            false,
1778            NoopNetwork::default(),
1779        );
1780
1781        let mut block = Block::default();
1782        block.header.number = 1;
1783        let block_hash = block.header.hash_slow();
1784        provider.add_block(block_hash, block);
1785
1786        let mut block_without_bal = Block::default();
1787        block_without_bal.header.number = 3;
1788        let block_without_bal_hash = block_without_bal.header.hash_slow();
1789        provider.add_block(block_without_bal_hash, block_without_bal);
1790
1791        let raw_bal = Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]);
1792        bal_store.insert(NumHash::new(1, block_hash), RawBal::new(raw_bal.clone())).unwrap();
1793
1794        let response = api.get_payload_bodies_by_range_v2(1, 3).await.unwrap();
1795
1796        assert_eq!(response.len(), 3);
1797        assert_eq!(response[0].as_ref().unwrap().block_access_list, Some(raw_bal));
1798        assert!(response[1].is_none());
1799        assert_eq!(response[2].as_ref().unwrap().block_access_list, None);
1800    }
1801
1802    struct EngineApiTestHandle {
1803        #[allow(dead_code)]
1804        chain_spec: Arc<ChainSpec>,
1805        provider: Arc<MockEthProvider>,
1806        from_api: UnboundedReceiver<BeaconEngineMessage<EthEngineTypes>>,
1807    }
1808
1809    #[tokio::test]
1810    async fn forwards_responses_to_consensus_engine() {
1811        let (mut handle, api) = setup_engine_api();
1812
1813        tokio::spawn(async move {
1814            let payload_v1 = ExecutionPayloadV1::from_block_slow(&Block::default());
1815            let execution_data = ExecutionData {
1816                payload: payload_v1.into(),
1817                sidecar: ExecutionPayloadSidecar::none(),
1818            };
1819
1820            api.new_payload_v1(execution_data).await.unwrap();
1821        });
1822        assert_matches!(handle.from_api.recv().await, Some(BeaconEngineMessage::NewPayload { .. }));
1823    }
1824
1825    #[tokio::test]
1826    async fn new_payload_v5_accepts_amsterdam_payloads() {
1827        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
1828        let provider = Arc::new(MockEthProvider::default());
1829        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
1830        let (to_engine, mut engine_rx) = unbounded_channel();
1831
1832        let api = EngineApi::new(
1833            provider,
1834            chain_spec.clone(),
1835            ConsensusEngineHandle::new(to_engine),
1836            payload_store.into(),
1837            NoopTransactionPool::default(),
1838            Runtime::test(),
1839            ClientVersionV1 {
1840                code: ClientCode::RH,
1841                name: "Reth".to_string(),
1842                version: "v0.0.0-test".to_string(),
1843                commit: "test".to_string(),
1844            },
1845            EngineCapabilities::default(),
1846            EthereumEngineValidator::new(chain_spec),
1847            false,
1848            NoopNetwork::default(),
1849        );
1850
1851        tokio::spawn(async move {
1852            let payload_v1 = ExecutionPayloadV1::from_block_slow(&Block::default());
1853            let payload = ExecutionPayloadV4 {
1854                payload_inner: ExecutionPayloadV3 {
1855                    payload_inner: ExecutionPayloadV2 {
1856                        payload_inner: payload_v1,
1857                        withdrawals: Vec::new(),
1858                    },
1859                    blob_gas_used: 0,
1860                    excess_blob_gas: 0,
1861                },
1862                block_access_list: Bytes::from_static(b"bal"),
1863                slot_number: 1,
1864            };
1865            let execution_data = ExecutionData {
1866                payload: payload.into(),
1867                sidecar: ExecutionPayloadSidecar::v4(
1868                    CancunPayloadFields {
1869                        versioned_hashes: Vec::new(),
1870                        parent_beacon_block_root: B256::ZERO,
1871                    },
1872                    PraguePayloadFields { requests: RequestsOrHash::Requests(Requests::default()) },
1873                ),
1874            };
1875
1876            api.new_payload_v5(execution_data).await.unwrap();
1877        });
1878
1879        assert_matches!(engine_rx.recv().await, Some(BeaconEngineMessage::NewPayload { .. }));
1880    }
1881
1882    #[derive(Clone)]
1883    struct TestNetworkInfo {
1884        syncing: bool,
1885    }
1886
1887    impl NetworkInfo for TestNetworkInfo {
1888        fn local_addr(&self) -> std::net::SocketAddr {
1889            (std::net::Ipv4Addr::UNSPECIFIED, 0).into()
1890        }
1891
1892        async fn network_status(&self) -> Result<NetworkStatus, NetworkError> {
1893            #[allow(deprecated)]
1894            Ok(NetworkStatus {
1895                client_version: "test".to_string(),
1896                protocol_version: 5,
1897                eth_protocol_info: EthProtocolInfo {
1898                    network: 1,
1899                    difficulty: None,
1900                    genesis: Default::default(),
1901                    config: Default::default(),
1902                    head: Default::default(),
1903                },
1904                capabilities: vec![],
1905            })
1906        }
1907
1908        fn chain_id(&self) -> u64 {
1909            1
1910        }
1911
1912        fn cell_custody(&self) -> &CellCustody {
1913            static CELL_CUSTODY: std::sync::OnceLock<CellCustody> = std::sync::OnceLock::new();
1914            CELL_CUSTODY.get_or_init(CellCustody::default)
1915        }
1916
1917        fn is_syncing(&self) -> bool {
1918            self.syncing
1919        }
1920
1921        fn is_initially_syncing(&self) -> bool {
1922            self.syncing
1923        }
1924    }
1925
1926    #[tokio::test]
1927    async fn get_blobs_v3_returns_null_when_syncing() {
1928        let chain_spec: Arc<ChainSpec> =
1929            Arc::new(ChainSpecBuilder::mainnet().osaka_activated().build());
1930        let provider = Arc::new(MockEthProvider::default());
1931        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
1932        let (to_engine, _engine_rx) = unbounded_channel::<BeaconEngineMessage<EthEngineTypes>>();
1933
1934        let api = EngineApi::new(
1935            provider,
1936            chain_spec.clone(),
1937            ConsensusEngineHandle::new(to_engine),
1938            payload_store.into(),
1939            NoopTransactionPool::default(),
1940            Runtime::test(),
1941            ClientVersionV1 {
1942                code: ClientCode::RH,
1943                name: "Reth".to_string(),
1944                version: "v0.0.0-test".to_string(),
1945                commit: "test".to_string(),
1946            },
1947            EngineCapabilities::default(),
1948            EthereumEngineValidator::new(chain_spec),
1949            false,
1950            TestNetworkInfo { syncing: true },
1951        );
1952
1953        let res = api.get_blobs_v3_metered(vec![B256::ZERO]);
1954        assert_matches!(res, Ok(None));
1955    }
1956
1957    #[tokio::test]
1958    async fn get_blobs_v4_returns_null_when_syncing() {
1959        let chain_spec: Arc<ChainSpec> =
1960            Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
1961        let provider = Arc::new(MockEthProvider::default());
1962        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
1963        let (to_engine, _engine_rx) = unbounded_channel::<BeaconEngineMessage<EthEngineTypes>>();
1964
1965        let api = EngineApi::new(
1966            provider,
1967            chain_spec.clone(),
1968            ConsensusEngineHandle::new(to_engine),
1969            payload_store.into(),
1970            NoopTransactionPool::default(),
1971            Runtime::test(),
1972            ClientVersionV1 {
1973                code: ClientCode::RH,
1974                name: "Reth".to_string(),
1975                version: "v0.0.0-test".to_string(),
1976                commit: "test".to_string(),
1977            },
1978            EngineCapabilities::default(),
1979            EthereumEngineValidator::new(chain_spec),
1980            false,
1981            TestNetworkInfo { syncing: true },
1982        );
1983
1984        let res = api.get_blobs_v4_metered(vec![B256::ZERO], B128::from(1u128));
1985        assert_matches!(res, Ok(None));
1986    }
1987
1988    #[tokio::test]
1989    async fn fcu_v4_updates_shared_cell_custody_before_forkchoice_result() {
1990        let chain_spec: Arc<ChainSpec> =
1991            Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
1992        let provider = Arc::new(MockEthProvider::default());
1993        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
1994        let (to_engine, mut engine_rx) = unbounded_channel();
1995        let network = NoopNetwork::default();
1996        let cell_custody = network.cell_custody().clone();
1997
1998        let api = EngineApi::new(
1999            provider,
2000            chain_spec.clone(),
2001            ConsensusEngineHandle::new(to_engine),
2002            payload_store.into(),
2003            NoopTransactionPool::default(),
2004            Runtime::test(),
2005            ClientVersionV1 {
2006                code: ClientCode::RH,
2007                name: "Reth".to_string(),
2008                version: "v0.0.0-test".to_string(),
2009                commit: "test".to_string(),
2010            },
2011            EngineCapabilities::default(),
2012            EthereumEngineValidator::new(chain_spec),
2013            false,
2014            network,
2015        );
2016
2017        let state = ForkchoiceState {
2018            head_block_hash: B256::from([0x33; 32]),
2019            safe_block_hash: B256::ZERO,
2020            finalized_block_hash: B256::ZERO,
2021        };
2022        let custody_columns = B128::from(0b1010u128);
2023
2024        let api_task = tokio::spawn(async move {
2025            api.fork_choice_updated_v4(state, None, Some(custody_columns)).await
2026        });
2027
2028        let request = tokio::time::timeout(std::time::Duration::from_secs(1), engine_rx.recv())
2029            .await
2030            .expect("timed out waiting for forkchoiceUpdated request")
2031            .expect("expected forkchoiceUpdated request");
2032        let response_tx = match request {
2033            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2034                assert!(payload_attrs.is_none());
2035                tx
2036            }
2037            other => panic!("unexpected engine message: {other:?}"),
2038        };
2039        assert_eq!(cell_custody.get(), custody_columns);
2040
2041        response_tx
2042            .send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
2043                PayloadStatusEnum::Valid,
2044            ))))
2045            .expect("send valid response");
2046
2047        api_task
2048            .await
2049            .expect("api task should not panic")
2050            .expect("forkchoiceUpdatedV4 should succeed");
2051        assert_eq!(cell_custody.get(), custody_columns);
2052    }
2053
2054    #[tokio::test]
2055    async fn fcu_v4_updates_shared_cell_custody_when_payload_attrs_invalid() {
2056        let chain_spec: Arc<ChainSpec> =
2057            Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
2058        let provider = Arc::new(MockEthProvider::default());
2059        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
2060        let (to_engine, mut engine_rx) = unbounded_channel();
2061        let network = NoopNetwork::default();
2062        let cell_custody = network.cell_custody().clone();
2063
2064        let api = EngineApi::new(
2065            provider,
2066            chain_spec.clone(),
2067            ConsensusEngineHandle::new(to_engine),
2068            payload_store.into(),
2069            NoopTransactionPool::default(),
2070            Runtime::test(),
2071            ClientVersionV1 {
2072                code: ClientCode::RH,
2073                name: "Reth".to_string(),
2074                version: "v0.0.0-test".to_string(),
2075                commit: "test".to_string(),
2076            },
2077            EngineCapabilities::default(),
2078            EthereumEngineValidator::new(chain_spec),
2079            false,
2080            network,
2081        );
2082
2083        let state = ForkchoiceState {
2084            head_block_hash: B256::from([0x44; 32]),
2085            safe_block_hash: B256::ZERO,
2086            finalized_block_hash: B256::ZERO,
2087        };
2088        let payload_attributes = PayloadAttributes {
2089            timestamp: 1,
2090            prev_randao: B256::ZERO,
2091            suggested_fee_recipient: Address::ZERO,
2092            withdrawals: Some(vec![]),
2093            parent_beacon_block_root: None,
2094            slot_number: None,
2095            target_gas_limit: None,
2096        };
2097        let custody_columns = B128::from(0b1010u128);
2098
2099        let api_task = tokio::spawn(async move {
2100            api.fork_choice_updated_v4(state, Some(payload_attributes), Some(custody_columns)).await
2101        });
2102
2103        let request = tokio::time::timeout(std::time::Duration::from_secs(1), engine_rx.recv())
2104            .await
2105            .expect("timed out waiting for forkchoiceUpdated request")
2106            .expect("expected forkchoiceUpdated request");
2107        let response_tx = match request {
2108            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2109                assert!(
2110                    payload_attrs.is_none(),
2111                    "when attrs are invalid, API should first evaluate forkchoice without attrs"
2112                );
2113                tx
2114            }
2115            other => panic!("unexpected engine message: {other:?}"),
2116        };
2117        assert_eq!(cell_custody.get(), custody_columns);
2118
2119        response_tx
2120            .send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
2121                PayloadStatusEnum::Valid,
2122            ))))
2123            .expect("send valid response");
2124
2125        let response = api_task.await.expect("api task should not panic");
2126        assert_matches!(
2127            response,
2128            Err(EngineApiError::EngineObjectValidationError(
2129                reth_payload_primitives::EngineObjectValidationError::PayloadAttributes(_)
2130            ))
2131        );
2132    }
2133
2134    #[tokio::test]
2135    async fn fcu_v3_syncing_precedes_invalid_payload_attributes_validation() {
2136        let (mut handle, api) = setup_engine_api();
2137
2138        let state = ForkchoiceState {
2139            head_block_hash: B256::from([0x11; 32]),
2140            safe_block_hash: B256::ZERO,
2141            finalized_block_hash: B256::ZERO,
2142        };
2143        let payload_attributes = PayloadAttributes {
2144            timestamp: 1,
2145            prev_randao: B256::ZERO,
2146            suggested_fee_recipient: Address::ZERO,
2147            withdrawals: Some(vec![]),
2148            // Invalid for V3/Cancun, but should be ignored if forkchoice is SYNCING.
2149            parent_beacon_block_root: None,
2150            slot_number: None,
2151            target_gas_limit: None,
2152        };
2153
2154        let api_task = tokio::spawn(async move {
2155            api.fork_choice_updated_v3(state, Some(payload_attributes)).await
2156        });
2157
2158        let request =
2159            tokio::time::timeout(std::time::Duration::from_secs(1), handle.from_api.recv())
2160                .await
2161                .expect("timed out waiting for forkchoiceUpdated request")
2162                .expect("expected forkchoiceUpdated request");
2163        let response_tx = match request {
2164            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2165                assert!(
2166                    payload_attrs.is_none(),
2167                    "FCU for syncing state should be evaluated before payload attributes"
2168                );
2169                tx
2170            }
2171            other => panic!("unexpected engine message: {other:?}"),
2172        };
2173
2174        response_tx.send(Ok(OnForkChoiceUpdated::syncing())).expect("send syncing response");
2175
2176        let response = api_task
2177            .await
2178            .expect("api task should not panic")
2179            .expect("forkchoiceUpdatedV3 should return a syncing response");
2180        assert!(response.payload_status.is_syncing());
2181        assert!(response.payload_id.is_none());
2182    }
2183
2184    #[tokio::test]
2185    async fn fcu_v3_valid_forkchoice_missing_beacon_root_returns_invalid_attributes() {
2186        let (mut handle, api) = setup_engine_api();
2187
2188        let state = ForkchoiceState {
2189            head_block_hash: B256::from([0x22; 32]),
2190            safe_block_hash: B256::ZERO,
2191            finalized_block_hash: B256::ZERO,
2192        };
2193        let payload_attributes = PayloadAttributes {
2194            timestamp: 1,
2195            prev_randao: B256::ZERO,
2196            suggested_fee_recipient: Address::ZERO,
2197            withdrawals: Some(vec![]),
2198            parent_beacon_block_root: None,
2199            slot_number: None,
2200            target_gas_limit: None,
2201        };
2202
2203        let api_task = tokio::spawn(async move {
2204            api.fork_choice_updated_v3(state, Some(payload_attributes)).await
2205        });
2206
2207        let request =
2208            tokio::time::timeout(std::time::Duration::from_secs(1), handle.from_api.recv())
2209                .await
2210                .expect("timed out waiting for forkchoiceUpdated request")
2211                .expect("expected forkchoiceUpdated request");
2212
2213        let response_tx = match request {
2214            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2215                assert!(
2216                    payload_attrs.is_none(),
2217                    "when attrs are invalid, API should first evaluate forkchoice without attrs"
2218                );
2219                tx
2220            }
2221            other => panic!("unexpected engine message: {other:?}"),
2222        };
2223
2224        response_tx
2225            .send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
2226                PayloadStatusEnum::Valid,
2227            ))))
2228            .expect("send valid response");
2229
2230        let response = api_task.await.expect("api task should not panic");
2231        assert_matches!(
2232            response,
2233            Err(EngineApiError::EngineObjectValidationError(
2234                reth_payload_primitives::EngineObjectValidationError::PayloadAttributes(_)
2235            ))
2236        );
2237
2238        match tokio::time::timeout(std::time::Duration::from_millis(100), handle.from_api.recv())
2239            .await
2240        {
2241            Err(_) | Ok(None) => {}
2242            Ok(Some(BeaconEngineMessage::ForkchoiceUpdated { .. })) => {
2243                panic!("no second forkchoiceUpdated call should be sent when attrs are invalid")
2244            }
2245            Ok(Some(other)) => panic!("unexpected engine message: {other:?}"),
2246        }
2247    }
2248
2249    // tests covering `engine_getPayloadBodiesByRange` and `engine_getPayloadBodiesByHash`
2250    mod get_payload_bodies {
2251        use super::*;
2252        use alloy_rpc_types_engine::ExecutionPayloadBodyV1;
2253        use reth_testing_utils::generators::{self, random_block_range, BlockRangeParams};
2254
2255        #[tokio::test]
2256        async fn invalid_params() {
2257            let (_, api) = setup_engine_api();
2258
2259            let by_range_tests = [
2260                // (start, count)
2261                (0, 0),
2262                (0, 1),
2263                (1, 0),
2264            ];
2265
2266            // test [EngineApiMessage::GetPayloadBodiesByRange]
2267            for (start, count) in by_range_tests {
2268                let res = api.get_payload_bodies_by_range_v1(start, count).await;
2269                assert_matches!(res, Err(EngineApiError::InvalidBodiesRange { .. }));
2270            }
2271        }
2272
2273        #[tokio::test]
2274        async fn request_too_large() {
2275            let (_, api) = setup_engine_api();
2276
2277            let request_count = MAX_PAYLOAD_BODIES_LIMIT + 1;
2278            let res = api.get_payload_bodies_by_range_v1(0, request_count).await;
2279            assert_matches!(res, Err(EngineApiError::PayloadRequestTooLarge { .. }));
2280        }
2281
2282        #[tokio::test]
2283        async fn returns_payload_bodies() {
2284            let mut rng = generators::rng();
2285            let (handle, api) = setup_engine_api();
2286
2287            let (start, count) = (1, 10);
2288            let blocks = random_block_range(
2289                &mut rng,
2290                start..=start + count - 1,
2291                BlockRangeParams { tx_count: 0..2, ..Default::default() },
2292            );
2293            handle
2294                .provider
2295                .extend_blocks(blocks.iter().cloned().map(|b| (b.hash(), b.into_block())));
2296
2297            let expected = blocks
2298                .iter()
2299                .cloned()
2300                .map(|b| Some(ExecutionPayloadBodyV1::from_block(b.into_block())))
2301                .collect::<Vec<_>>();
2302
2303            let res = api.get_payload_bodies_by_range_v1(start, count).await.unwrap();
2304            assert_eq!(res, expected);
2305        }
2306
2307        #[tokio::test]
2308        async fn returns_payload_bodies_with_gaps() {
2309            let mut rng = generators::rng();
2310            let (handle, api) = setup_engine_api();
2311
2312            let (start, count) = (1, 100);
2313            let blocks = random_block_range(
2314                &mut rng,
2315                start..=start + count - 1,
2316                BlockRangeParams { tx_count: 0..2, ..Default::default() },
2317            );
2318
2319            // Insert only blocks in ranges 1-25 and 50-75
2320            let first_missing_range = 26..=50;
2321            let second_missing_range = 76..=100;
2322            handle.provider.extend_blocks(
2323                blocks
2324                    .iter()
2325                    .filter(|b| {
2326                        !first_missing_range.contains(&b.number) &&
2327                            !second_missing_range.contains(&b.number)
2328                    })
2329                    .map(|b| (b.hash(), b.clone().into_block())),
2330            );
2331
2332            let expected = blocks
2333                .iter()
2334                // filter anything after the second missing range to ensure we don't expect trailing
2335                // `None`s
2336                .filter(|b| !second_missing_range.contains(&b.number))
2337                .cloned()
2338                .map(|b| {
2339                    if first_missing_range.contains(&b.number) {
2340                        None
2341                    } else {
2342                        Some(ExecutionPayloadBodyV1::from_block(b.into_block()))
2343                    }
2344                })
2345                .collect::<Vec<_>>();
2346
2347            let res = api.get_payload_bodies_by_range_v1(start, count).await.unwrap();
2348            assert_eq!(res, expected);
2349
2350            let expected = blocks
2351                .iter()
2352                .cloned()
2353                // ensure we still return trailing `None`s here because by-hash will not be aware
2354                // of the missing block's number, and cannot compare it to the current best block
2355                .map(|b| {
2356                    if first_missing_range.contains(&b.number) ||
2357                        second_missing_range.contains(&b.number)
2358                    {
2359                        None
2360                    } else {
2361                        Some(ExecutionPayloadBodyV1::from_block(b.into_block()))
2362                    }
2363                })
2364                .collect::<Vec<_>>();
2365
2366            let hashes = blocks.iter().map(|b| b.hash()).collect();
2367            let res = api.get_payload_bodies_by_hash_v1(hashes).await.unwrap();
2368            assert_eq!(res, expected);
2369        }
2370    }
2371}