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    eip7594::BlobCellMask,
9    eip7685::RequestsOrHash,
10};
11use alloy_primitives::{BlockHash, BlockNumber, Bytes, Sealable, B128, B256, U64};
12use alloy_rpc_types_engine::{
13    BogotaPayloadFields, CancunPayloadFields, ClientVersionV1, ExecutionData,
14    ExecutionPayloadBodiesV1, ExecutionPayloadBodiesV2, ExecutionPayloadBodyV1,
15    ExecutionPayloadBodyV2, ExecutionPayloadInputV2, ExecutionPayloadSidecar, ExecutionPayloadV1,
16    ExecutionPayloadV3, ExecutionPayloadV4, ForkchoiceState, ForkchoiceUpdated,
17    ForkchoiceUpdatedResponseV2, PayloadId, PayloadStatus, PayloadStatusV2, PraguePayloadFields,
18    MAX_BYTES_PER_INCLUSION_LIST,
19};
20use jsonrpsee_core::{server::RpcModule, RpcResult};
21use reth_chainspec::EthereumHardforks;
22use reth_engine_primitives::{ConsensusEngineHandle, EngineApiValidator, EngineTypes};
23use reth_network_api::{CellCustody, NetworkInfo};
24use reth_payload_builder::PayloadStore;
25use reth_payload_primitives::{
26    validate_payload_timestamp, EngineApiMessageVersion, MessageValidationKind,
27    PayloadOrAttributes, PayloadTypes,
28};
29use reth_primitives_traits::{AlloyBlockHeader, Block, BlockBody};
30use reth_rpc_api::{EngineApiServer, IntoEngineApiRpcModule};
31use reth_storage_api::{BalProvider, BlockReader, StateProviderFactory};
32use reth_tasks::Runtime;
33use reth_transaction_pool::{BestTransactions, TransactionPool};
34use std::{
35    sync::Arc,
36    time::{Instant, SystemTime},
37};
38use tokio::sync::oneshot;
39use tracing::{debug, trace, warn};
40
41/// The Engine API response sender.
42pub type EngineApiSender<Ok> = oneshot::Sender<EngineApiResult<Ok>>;
43
44/// The upper limit for payload bodies request.
45const MAX_PAYLOAD_BODIES_LIMIT: u64 = 1024;
46
47/// The upper limit for blobs in `engine_getBlobsVx`.
48const MAX_BLOB_LIMIT: usize = 128;
49
50/// The Engine API implementation that grants the Consensus layer access to data and
51/// functions in the Execution layer that are crucial for the consensus process.
52///
53/// This type is generic over [`EngineTypes`] and intended to be used as the entrypoint for engine
54/// API processing. It can be reused by other non L1 engine APIs that deviate from the L1 spec but
55/// are still follow the engine API model.
56///
57/// ## Implementers
58///
59/// Implementing support for an engine API jsonrpsee RPC handler is done by defining the engine API
60/// server trait and implementing it on a type that can either wrap this [`EngineApi`] type or
61/// use a custom [`EngineTypes`] implementation if it mirrors ethereum's versioned engine API
62/// endpoints (e.g. opstack).
63/// See also [`EngineApiServer`] implementation for this type which is the
64/// L1 implementation.
65pub struct EngineApi<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSpec> {
66    inner: Arc<EngineApiInner<Provider, PayloadT, Pool, Validator, ChainSpec>>,
67}
68
69impl<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSpec>
70    EngineApi<Provider, PayloadT, Pool, Validator, ChainSpec>
71{
72    /// Returns the configured chainspec.
73    pub fn chain_spec(&self) -> &Arc<ChainSpec> {
74        &self.inner.chain_spec
75    }
76
77    /// Returns the configured client version.
78    pub fn client_version(&self) -> &ClientVersionV1 {
79        &self.inner.client
80    }
81}
82
83impl<Provider, PayloadT, Pool, Validator, ChainSpec>
84    EngineApi<Provider, PayloadT, Pool, Validator, ChainSpec>
85where
86    Provider: BlockReader + StateProviderFactory + BalProvider + 'static,
87    PayloadT: PayloadTypes,
88    Pool: TransactionPool + 'static,
89    Validator: EngineApiValidator<PayloadT>,
90    ChainSpec: EthereumHardforks + Send + Sync + 'static,
91{
92    /// Create new instance of [`EngineApi`].
93    #[expect(clippy::too_many_arguments)]
94    pub fn new(
95        provider: Provider,
96        chain_spec: Arc<ChainSpec>,
97        beacon_consensus: ConsensusEngineHandle<PayloadT>,
98        payload_store: PayloadStore<PayloadT>,
99        tx_pool: Pool,
100        task_spawner: Runtime,
101        client: ClientVersionV1,
102        capabilities: EngineCapabilities,
103        validator: Validator,
104        accept_execution_requests_hash: bool,
105        network: impl NetworkInfo + 'static,
106    ) -> Self {
107        let cell_custody = network.cell_custody().clone();
108        let is_syncing = Arc::new(move || network.is_syncing());
109        let inner = Arc::new(EngineApiInner {
110            provider,
111            chain_spec,
112            beacon_consensus,
113            payload_store,
114            task_spawner,
115            metrics: EngineApiMetrics::default(),
116            client,
117            capabilities,
118            tx_pool,
119            validator,
120            accept_execution_requests_hash,
121            cell_custody,
122            is_syncing,
123        });
124        Self { inner }
125    }
126
127    /// Fetches the client version.
128    pub fn get_client_version_v1(
129        &self,
130        _client: ClientVersionV1,
131    ) -> EngineApiResult<Vec<ClientVersionV1>> {
132        Ok(vec![self.inner.client.clone()])
133    }
134
135    /// Fetches the timestamp of the payload with the given id.
136    async fn get_payload_timestamp(&self, payload_id: PayloadId) -> EngineApiResult<u64> {
137        Ok(self
138            .inner
139            .payload_store
140            .payload_timestamp(payload_id)
141            .await
142            .ok_or(EngineApiError::UnknownPayload)??)
143    }
144
145    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_newpayloadv1>
146    /// Caution: This should not accept the `withdrawals` field
147    pub async fn new_payload_v1(
148        &self,
149        payload: PayloadT::ExecutionData,
150    ) -> EngineApiResult<PayloadStatus> {
151        let payload_or_attrs = PayloadOrAttributes::<
152            '_,
153            PayloadT::ExecutionData,
154            PayloadT::PayloadAttributes,
155        >::from_execution_payload(&payload);
156
157        self.inner
158            .validator
159            .validate_version_specific_fields(EngineApiMessageVersion::V1, payload_or_attrs)?;
160
161        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
162    }
163
164    /// Metered version of `new_payload_v1`.
165    pub async fn new_payload_v1_metered(
166        &self,
167        payload: PayloadT::ExecutionData,
168    ) -> EngineApiResult<PayloadStatus> {
169        let start = Instant::now();
170        let res = Self::new_payload_v1(self, payload).await;
171        let elapsed = start.elapsed();
172        self.inner.metrics.latency.new_payload_v1.record(elapsed);
173        res
174    }
175
176    /// See also <https://github.com/ethereum/execution-apis/blob/584905270d8ad665718058060267061ecfd79ca5/src/engine/shanghai.md#engine_newpayloadv2>
177    pub async fn new_payload_v2(
178        &self,
179        payload: PayloadT::ExecutionData,
180    ) -> EngineApiResult<PayloadStatus> {
181        let payload_or_attrs = PayloadOrAttributes::<
182            '_,
183            PayloadT::ExecutionData,
184            PayloadT::PayloadAttributes,
185        >::from_execution_payload(&payload);
186        self.inner
187            .validator
188            .validate_version_specific_fields(EngineApiMessageVersion::V2, payload_or_attrs)?;
189        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
190    }
191
192    /// Metered version of `new_payload_v2`.
193    pub async fn new_payload_v2_metered(
194        &self,
195        payload: PayloadT::ExecutionData,
196    ) -> EngineApiResult<PayloadStatus> {
197        let start = Instant::now();
198        let res = Self::new_payload_v2(self, payload).await;
199        let elapsed = start.elapsed();
200        self.inner.metrics.latency.new_payload_v2.record(elapsed);
201        res
202    }
203
204    /// See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_newpayloadv3>
205    pub async fn new_payload_v3(
206        &self,
207        payload: PayloadT::ExecutionData,
208    ) -> EngineApiResult<PayloadStatus> {
209        let payload_or_attrs = PayloadOrAttributes::<
210            '_,
211            PayloadT::ExecutionData,
212            PayloadT::PayloadAttributes,
213        >::from_execution_payload(&payload);
214        self.inner
215            .validator
216            .validate_version_specific_fields(EngineApiMessageVersion::V3, payload_or_attrs)?;
217
218        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
219    }
220
221    /// Metrics version of `new_payload_v3`
222    pub async fn new_payload_v3_metered(
223        &self,
224        payload: PayloadT::ExecutionData,
225    ) -> RpcResult<PayloadStatus> {
226        let start = Instant::now();
227
228        let res = Self::new_payload_v3(self, payload).await;
229        let elapsed = start.elapsed();
230        self.inner.metrics.latency.new_payload_v3.record(elapsed);
231        Ok(res?)
232    }
233
234    /// See also <https://github.com/ethereum/execution-apis/blob/7907424db935b93c2fe6a3c0faab943adebe8557/src/engine/prague.md#engine_newpayloadv4>
235    pub async fn new_payload_v4(
236        &self,
237        payload: PayloadT::ExecutionData,
238    ) -> EngineApiResult<PayloadStatus> {
239        let payload_or_attrs = PayloadOrAttributes::<
240            '_,
241            PayloadT::ExecutionData,
242            PayloadT::PayloadAttributes,
243        >::from_execution_payload(&payload);
244        self.inner
245            .validator
246            .validate_version_specific_fields(EngineApiMessageVersion::V4, payload_or_attrs)?;
247
248        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
249    }
250
251    /// Metrics version of `new_payload_v4`
252    pub async fn new_payload_v4_metered(
253        &self,
254        payload: PayloadT::ExecutionData,
255    ) -> RpcResult<PayloadStatus> {
256        let start = Instant::now();
257        let res = Self::new_payload_v4(self, payload).await;
258
259        let elapsed = start.elapsed();
260        self.inner.metrics.latency.new_payload_v4.record(elapsed);
261        Ok(res?)
262    }
263
264    /// Handler for `engine_newPayloadV5`
265    ///
266    /// Post-Amsterdam payload handler.
267    ///
268    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_newpayloadv5>
269    pub async fn new_payload_v5(
270        &self,
271        payload: PayloadT::ExecutionData,
272    ) -> EngineApiResult<PayloadStatus> {
273        let payload_or_attrs = PayloadOrAttributes::<
274            '_,
275            PayloadT::ExecutionData,
276            PayloadT::PayloadAttributes,
277        >::from_execution_payload(&payload);
278        self.inner
279            .validator
280            .validate_version_specific_fields(EngineApiMessageVersion::V5, payload_or_attrs)?;
281        Ok(self.inner.beacon_consensus.new_payload(payload).await?)
282    }
283
284    /// Metrics version of `new_payload_v5`
285    pub async fn new_payload_v5_metered(
286        &self,
287        payload: PayloadT::ExecutionData,
288    ) -> RpcResult<PayloadStatus> {
289        let start = Instant::now();
290        let res = Self::new_payload_v5(self, payload).await;
291        let elapsed = start.elapsed();
292        self.inner.metrics.latency.new_payload_v5.record(elapsed);
293        Ok(res?)
294    }
295
296    /// Handler for `engine_newPayloadV6`.
297    ///
298    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#engine_newpayloadv6>
299    pub async fn new_payload_v6(
300        &self,
301        payload: PayloadT::ExecutionData,
302    ) -> EngineApiResult<PayloadStatusV2> {
303        let payload_or_attrs = PayloadOrAttributes::<
304            '_,
305            PayloadT::ExecutionData,
306            PayloadT::PayloadAttributes,
307        >::from_execution_payload(&payload);
308        self.inner
309            .validator
310            .validate_version_specific_fields(EngineApiMessageVersion::V6, payload_or_attrs)?;
311
312        Ok(self.inner.beacon_consensus.new_payload(payload).await?.into())
313    }
314
315    /// Metrics version of `new_payload_v6`.
316    pub async fn new_payload_v6_metered(
317        &self,
318        payload: PayloadT::ExecutionData,
319    ) -> EngineApiResult<PayloadStatusV2> {
320        let start = Instant::now();
321        let result = Self::new_payload_v6(self, payload).await;
322        self.inner.metrics.latency.new_payload_v6.record(start.elapsed());
323        result
324    }
325
326    /// Returns whether the engine accepts execution requests hash.
327    pub fn accept_execution_requests_hash(&self) -> bool {
328        self.inner.accept_execution_requests_hash
329    }
330}
331
332impl<Provider, EngineT, Pool, Validator, ChainSpec>
333    EngineApi<Provider, EngineT, Pool, Validator, ChainSpec>
334where
335    Provider: BlockReader + StateProviderFactory + BalProvider + 'static,
336    EngineT: EngineTypes,
337    Pool: TransactionPool + 'static,
338    Validator: EngineApiValidator<EngineT>,
339    ChainSpec: EthereumHardforks + Send + Sync + 'static,
340{
341    /// Sends a message to the beacon consensus engine to update the fork choice _without_
342    /// withdrawals.
343    ///
344    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_forkchoiceUpdatedV1>
345    ///
346    /// Caution: This should not accept the `withdrawals` field
347    pub async fn fork_choice_updated_v1(
348        &self,
349        state: ForkchoiceState,
350        payload_attrs: Option<EngineT::PayloadAttributes>,
351    ) -> EngineApiResult<ForkchoiceUpdated> {
352        self.validate_and_execute_forkchoice(EngineApiMessageVersion::V1, state, payload_attrs)
353            .await
354    }
355
356    /// Metrics version of `fork_choice_updated_v1`
357    pub async fn fork_choice_updated_v1_metered(
358        &self,
359        state: ForkchoiceState,
360        payload_attrs: Option<EngineT::PayloadAttributes>,
361    ) -> EngineApiResult<ForkchoiceUpdated> {
362        let start = Instant::now();
363        let res = Self::fork_choice_updated_v1(self, state, payload_attrs).await;
364        self.inner.metrics.latency.fork_choice_updated_v1.record(start.elapsed());
365        res
366    }
367
368    /// Sends a message to the beacon consensus engine to update the fork choice _with_ withdrawals,
369    /// but only _after_ shanghai.
370    ///
371    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/shanghai.md#engine_forkchoiceupdatedv2>
372    pub async fn fork_choice_updated_v2(
373        &self,
374        state: ForkchoiceState,
375        payload_attrs: Option<EngineT::PayloadAttributes>,
376    ) -> EngineApiResult<ForkchoiceUpdated> {
377        self.validate_and_execute_forkchoice(EngineApiMessageVersion::V2, state, payload_attrs)
378            .await
379    }
380
381    /// Metrics version of `fork_choice_updated_v2`
382    pub async fn fork_choice_updated_v2_metered(
383        &self,
384        state: ForkchoiceState,
385        payload_attrs: Option<EngineT::PayloadAttributes>,
386    ) -> EngineApiResult<ForkchoiceUpdated> {
387        let start = Instant::now();
388        let res = Self::fork_choice_updated_v2(self, state, payload_attrs).await;
389        self.inner.metrics.latency.fork_choice_updated_v2.record(start.elapsed());
390        res
391    }
392
393    /// Sends a message to the beacon consensus engine to update the fork choice _with_ withdrawals,
394    /// but only _after_ cancun.
395    ///
396    /// See also  <https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#engine_forkchoiceupdatedv3>
397    pub async fn fork_choice_updated_v3(
398        &self,
399        state: ForkchoiceState,
400        payload_attrs: Option<EngineT::PayloadAttributes>,
401    ) -> EngineApiResult<ForkchoiceUpdated> {
402        self.validate_and_execute_forkchoice(EngineApiMessageVersion::V3, state, payload_attrs)
403            .await
404    }
405
406    /// Metrics version of `fork_choice_updated_v3`
407    pub async fn fork_choice_updated_v3_metered(
408        &self,
409        state: ForkchoiceState,
410        payload_attrs: Option<EngineT::PayloadAttributes>,
411    ) -> EngineApiResult<ForkchoiceUpdated> {
412        let start = Instant::now();
413        let res = Self::fork_choice_updated_v3(self, state, payload_attrs).await;
414        self.inner.metrics.latency.fork_choice_updated_v3.record(start.elapsed());
415        res
416    }
417
418    /// Sends a message to the beacon consensus engine to update the fork choice _with_ slot number,
419    /// but only _after_ amsterdam.
420    ///
421    /// See also  <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_forkchoiceupdatedv4>
422    pub async fn fork_choice_updated_v4(
423        &self,
424        state: ForkchoiceState,
425        payload_attrs: Option<EngineT::PayloadAttributes>,
426        custody_columns: Option<B128>,
427    ) -> EngineApiResult<ForkchoiceUpdated> {
428        if let Some(custody_columns) = custody_columns {
429            self.inner.cell_custody.set_from_engine_api(custody_columns);
430        }
431        self.validate_and_execute_forkchoice(EngineApiMessageVersion::V4, state, payload_attrs)
432            .await
433    }
434
435    /// Metrics version of `fork_choice_updated_v4`
436    pub async fn fork_choice_updated_v4_metered(
437        &self,
438        state: ForkchoiceState,
439        payload_attrs: Option<EngineT::PayloadAttributes>,
440        custody_columns: Option<B128>,
441    ) -> EngineApiResult<ForkchoiceUpdated> {
442        let start = Instant::now();
443        let res = Self::fork_choice_updated_v4(self, state, payload_attrs, custody_columns).await;
444        self.inner.metrics.latency.fork_choice_updated_v4.record(start.elapsed());
445        res
446    }
447
448    /// Handler for `engine_forkchoiceUpdatedV5`.
449    ///
450    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#engine_forkchoiceupdatedv5>
451    pub async fn fork_choice_updated_v5(
452        &self,
453        state: ForkchoiceState,
454        payload_attrs: Option<EngineT::PayloadAttributes>,
455        custody_columns: Option<B128>,
456    ) -> EngineApiResult<ForkchoiceUpdatedResponseV2> {
457        if let Some(custody_columns) = custody_columns {
458            self.inner.cell_custody.set_from_engine_api(custody_columns);
459        }
460
461        // Todo: Validate IL and populate `inclusion_list_satisfied` properly and test.
462        Ok(self
463            .validate_and_execute_forkchoice(EngineApiMessageVersion::V5, state, payload_attrs)
464            .await?
465            .into())
466    }
467
468    /// Metrics version of `fork_choice_updated_v5`
469    pub async fn fork_choice_updated_v5_metered(
470        &self,
471        state: ForkchoiceState,
472        payload_attrs: Option<EngineT::PayloadAttributes>,
473        custody_columns: Option<B128>,
474    ) -> EngineApiResult<ForkchoiceUpdatedResponseV2> {
475        let start = Instant::now();
476        let res = Self::fork_choice_updated_v5(self, state, payload_attrs, custody_columns).await;
477        self.inner.metrics.latency.fork_choice_updated_v5.record(start.elapsed());
478        res
479    }
480
481    /// Builds an EIP-7805 inclusion list from the local transaction pool.
482    pub fn get_inclusion_list_v1(&self) -> EngineApiResult<Vec<Bytes>> {
483        let mut total_size = 0;
484        let mut inclusion_list = Vec::new();
485
486        for pool_tx in self.inner.tx_pool.best_transactions().without_blobs().without_updates() {
487            let encoded = pool_tx.encoded_2718_consensus();
488            let new_size = total_size + encoded.len();
489            if new_size > MAX_BYTES_PER_INCLUSION_LIST as usize {
490                break
491            }
492
493            total_size = new_size;
494            inclusion_list.push(encoded);
495        }
496
497        Ok(inclusion_list)
498    }
499
500    /// Metrics version of `get_inclusion_list_v1`.
501    pub fn get_inclusion_list_v1_metered(&self) -> EngineApiResult<Vec<Bytes>> {
502        let start = Instant::now();
503        let result = Self::get_inclusion_list_v1(self);
504        self.inner.metrics.latency.get_inclusion_list_v1.record(start.elapsed());
505        result
506    }
507
508    /// Helper function for retrieving the build payload by id.
509    async fn get_built_payload(
510        &self,
511        payload_id: PayloadId,
512    ) -> EngineApiResult<EngineT::BuiltPayload> {
513        self.inner
514            .payload_store
515            .resolve(payload_id)
516            .await
517            .ok_or(EngineApiError::UnknownPayload)?
518            .map_err(|_| EngineApiError::UnknownPayload)
519    }
520
521    /// Helper function for validating the payload timestamp and retrieving & converting the payload
522    /// into desired envelope.
523    async fn get_payload_inner<R>(
524        &self,
525        payload_id: PayloadId,
526        version: EngineApiMessageVersion,
527    ) -> EngineApiResult<R>
528    where
529        EngineT::BuiltPayload: TryInto<R>,
530    {
531        // Validate timestamp according to engine rules
532        // Enforces Osaka restrictions on `getPayloadV4`.
533        let timestamp = self.get_payload_timestamp(payload_id).await?;
534        validate_payload_timestamp(
535            &self.inner.chain_spec,
536            version,
537            timestamp,
538            MessageValidationKind::GetPayload,
539        )?;
540
541        // Now resolve the payload
542        self.get_built_payload(payload_id).await?.try_into().map_err(|_| {
543            warn!(?version, "could not transform built payload");
544            EngineApiError::UnknownPayload
545        })
546    }
547
548    /// Returns the most recent version of the payload that is available in the corresponding
549    /// payload build process at the time of receiving this call.
550    ///
551    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_getPayloadV1>
552    ///
553    /// Caution: This should not return the `withdrawals` field
554    ///
555    /// Note:
556    /// > Provider software MAY stop the corresponding build process after serving this call.
557    pub async fn get_payload_v1(
558        &self,
559        payload_id: PayloadId,
560    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV1> {
561        self.get_built_payload(payload_id).await?.try_into().map_err(|_| {
562            warn!(version = ?EngineApiMessageVersion::V1, "could not transform built payload");
563            EngineApiError::UnknownPayload
564        })
565    }
566
567    /// Metrics version of `get_payload_v1`
568    pub async fn get_payload_v1_metered(
569        &self,
570        payload_id: PayloadId,
571    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV1> {
572        let start = Instant::now();
573        let res = Self::get_payload_v1(self, payload_id).await;
574        self.inner.metrics.latency.get_payload_v1.record(start.elapsed());
575        res
576    }
577
578    /// Returns the most recent version of the payload that is available in the corresponding
579    /// payload build process at the time of receiving this call.
580    ///
581    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/shanghai.md#engine_getpayloadv2>
582    ///
583    /// Note:
584    /// > Provider software MAY stop the corresponding build process after serving this call.
585    pub async fn get_payload_v2(
586        &self,
587        payload_id: PayloadId,
588    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV2> {
589        self.get_payload_inner(payload_id, EngineApiMessageVersion::V2).await
590    }
591
592    /// Metrics version of `get_payload_v2`
593    pub async fn get_payload_v2_metered(
594        &self,
595        payload_id: PayloadId,
596    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV2> {
597        let start = Instant::now();
598        let res = Self::get_payload_v2(self, payload_id).await;
599        self.inner.metrics.latency.get_payload_v2.record(start.elapsed());
600        res
601    }
602
603    /// Returns the most recent version of the payload that is available in the corresponding
604    /// payload build process at the time of receiving this call.
605    ///
606    /// See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_getpayloadv3>
607    ///
608    /// Note:
609    /// > Provider software MAY stop the corresponding build process after serving this call.
610    pub async fn get_payload_v3(
611        &self,
612        payload_id: PayloadId,
613    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV3> {
614        self.get_payload_inner(payload_id, EngineApiMessageVersion::V3).await
615    }
616
617    /// Metrics version of `get_payload_v3`
618    pub async fn get_payload_v3_metered(
619        &self,
620        payload_id: PayloadId,
621    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV3> {
622        let start = Instant::now();
623        let res = Self::get_payload_v3(self, payload_id).await;
624        self.inner.metrics.latency.get_payload_v3.record(start.elapsed());
625        res
626    }
627
628    /// Returns the most recent version of the payload that is available in the corresponding
629    /// payload build process at the time of receiving this call.
630    ///
631    /// See also <https://github.com/ethereum/execution-apis/blob/7907424db935b93c2fe6a3c0faab943adebe8557/src/engine/prague.md#engine_getpayloadv4>
632    ///
633    /// Note:
634    /// > Provider software MAY stop the corresponding build process after serving this call.
635    pub async fn get_payload_v4(
636        &self,
637        payload_id: PayloadId,
638    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV4> {
639        self.get_payload_inner(payload_id, EngineApiMessageVersion::V4).await
640    }
641
642    /// Metrics version of `get_payload_v4`
643    pub async fn get_payload_v4_metered(
644        &self,
645        payload_id: PayloadId,
646    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV4> {
647        let start = Instant::now();
648        let res = Self::get_payload_v4(self, payload_id).await;
649        self.inner.metrics.latency.get_payload_v4.record(start.elapsed());
650        res
651    }
652
653    /// Handler for `engine_getPayloadV5`
654    ///
655    /// Returns the most recent version of the payload that is available in the corresponding
656    /// payload build process at the time of receiving this call.
657    ///
658    /// See also <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#engine_getpayloadv5>
659    ///
660    /// Note:
661    /// > Provider software MAY stop the corresponding build process after serving this call.
662    pub async fn get_payload_v5(
663        &self,
664        payload_id: PayloadId,
665    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV5> {
666        self.get_payload_inner(payload_id, EngineApiMessageVersion::V5).await
667    }
668
669    /// Metrics version of `get_payload_v5`
670    pub async fn get_payload_v5_metered(
671        &self,
672        payload_id: PayloadId,
673    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV5> {
674        let start = Instant::now();
675        let res = Self::get_payload_v5(self, payload_id).await;
676        self.inner.metrics.latency.get_payload_v5.record(start.elapsed());
677        res
678    }
679
680    /// Handler for `engine_getPayloadV6`
681    ///
682    /// Post-Amsterdam payload handler that includes Block Access Lists (BAL).
683    ///
684    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_getpayloadv6>
685    pub async fn get_payload_v6(
686        &self,
687        payload_id: PayloadId,
688    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV6> {
689        self.get_payload_inner(payload_id, EngineApiMessageVersion::V6).await
690    }
691
692    /// Metrics version of `get_payload_v6`
693    pub async fn get_payload_v6_metered(
694        &self,
695        payload_id: PayloadId,
696    ) -> EngineApiResult<EngineT::ExecutionPayloadEnvelopeV6> {
697        let start = Instant::now();
698        let res = Self::get_payload_v6(self, payload_id).await;
699        self.inner.metrics.latency.get_payload_v6.record(start.elapsed());
700        res
701    }
702
703    /// Fetches all the blocks for the provided range starting at `start`, containing `count`
704    /// blocks and returns the mapped payload bodies.
705    pub async fn get_payload_bodies_by_range_with<F, R>(
706        &self,
707        start: BlockNumber,
708        count: u64,
709        f: F,
710    ) -> EngineApiResult<Vec<Option<R>>>
711    where
712        F: Fn(Provider::Block) -> R + Send + 'static,
713        R: Send + 'static,
714    {
715        let (tx, rx) = oneshot::channel();
716        let inner = self.inner.clone();
717
718        self.inner.task_spawner.spawn_blocking_task(async move {
719            if count > MAX_PAYLOAD_BODIES_LIMIT {
720                tx.send(Err(EngineApiError::PayloadRequestTooLarge { len: count })).ok();
721                return;
722            }
723
724            if start == 0 || count == 0 {
725                tx.send(Err(EngineApiError::InvalidBodiesRange { start, count })).ok();
726                return;
727            }
728
729            let mut result = Vec::with_capacity(count as usize);
730
731            // -1 so range is inclusive
732            let mut end = start.saturating_add(count - 1);
733
734            // > Client software MUST NOT return trailing null values if the request extends past the current latest known block.
735            // truncate the end if it's greater than the last block
736            if let Ok(best_block) = inner.provider.best_block_number()
737                && end > best_block {
738                    end = best_block;
739                }
740
741            // Check if the requested range starts before the earliest available block due to pruning/expiry
742            let earliest_block = inner.provider.earliest_block_number().unwrap_or(0);
743            for num in start..=end {
744                if tx.is_closed() {
745                    return;
746                }
747
748                if num < earliest_block {
749                    result.push(None);
750                    continue;
751                }
752                let block_result = inner.provider.block(BlockHashOrNumber::Number(num));
753                match block_result {
754                    Ok(block) => {
755                        result.push(block.map(&f));
756                    }
757                    Err(err) => {
758                        tx.send(Err(EngineApiError::Internal(Box::new(err)))).ok();
759                        return;
760                    }
761                };
762            }
763            tx.send(Ok(result)).ok();
764        });
765
766        rx.await.map_err(|err| EngineApiError::Internal(Box::new(err)))?
767    }
768
769    /// Returns payload bodies and their timestamps from the same block read.
770    ///
771    /// SSZ transports use the timestamp to select the block's fork schema. Reading it
772    /// separately by block number could pair a body with another block after a reorg.
773    pub async fn get_payload_bodies_by_range_with_timestamps(
774        &self,
775        start: BlockNumber,
776        count: u64,
777        include_bal: bool,
778    ) -> EngineApiResult<Vec<Option<(u64, ExecutionPayloadBodyV2)>>> {
779        let bodies = self
780            .get_payload_bodies_by_range_with(start, count, Self::payload_body_with_timestamp)
781            .await?;
782        self.attach_payload_body_bals(bodies, include_bal).await
783    }
784
785    /// Records SSZ body requests in the corresponding JSON-RPC V1/V2 latency histogram.
786    pub async fn get_payload_bodies_by_range_with_timestamps_metered(
787        &self,
788        start: BlockNumber,
789        count: u64,
790        include_bal: bool,
791    ) -> EngineApiResult<Vec<Option<(u64, ExecutionPayloadBodyV2)>>> {
792        let start_time = Instant::now();
793        let result =
794            self.get_payload_bodies_by_range_with_timestamps(start, count, include_bal).await;
795        let latency = &self.inner.metrics.latency;
796        if include_bal {
797            latency.get_payload_bodies_by_range_v2.record(start_time.elapsed());
798        } else {
799            latency.get_payload_bodies_by_range_v1.record(start_time.elapsed());
800        }
801        result
802    }
803
804    /// Returns the execution payload bodies by the range starting at `start`, containing `count`
805    /// blocks.
806    ///
807    /// WARNING: This method is associated with the `BeaconBlocksByRange` message in the consensus
808    /// layer p2p specification, meaning the input should be treated as untrusted or potentially
809    /// adversarial.
810    ///
811    /// Implementers should take care when acting on the input to this method, specifically
812    /// ensuring that the range is limited properly, and that the range boundaries are computed
813    /// correctly and without panics.
814    pub async fn get_payload_bodies_by_range_v1(
815        &self,
816        start: BlockNumber,
817        count: u64,
818    ) -> EngineApiResult<ExecutionPayloadBodiesV1> {
819        self.get_payload_bodies_by_range_with(start, count, |block| ExecutionPayloadBodyV1 {
820            transactions: block.body().encoded_2718_transactions(),
821            withdrawals: block.body().withdrawals().cloned().map(Withdrawals::into_inner),
822        })
823        .await
824    }
825
826    /// Metrics version of `get_payload_bodies_by_range_v1`
827    pub async fn get_payload_bodies_by_range_v1_metered(
828        &self,
829        start: BlockNumber,
830        count: u64,
831    ) -> EngineApiResult<ExecutionPayloadBodiesV1> {
832        let start_time = Instant::now();
833        let res = Self::get_payload_bodies_by_range_v1(self, start, count).await;
834        self.inner.metrics.latency.get_payload_bodies_by_range_v1.record(start_time.elapsed());
835        res
836    }
837
838    /// Returns the execution payload bodies by the range (V2).
839    ///
840    /// V2 includes the `block_access_list` field for EIP-7928 BAL support.
841    pub async fn get_payload_bodies_by_range_v2(
842        &self,
843        start: BlockNumber,
844        count: u64,
845    ) -> EngineApiResult<ExecutionPayloadBodiesV2> {
846        Ok(self
847            .get_payload_bodies_by_range_with_timestamps(start, count, true)
848            .await?
849            .into_iter()
850            .map(|body| body.map(|(_, body)| body))
851            .collect())
852    }
853
854    /// Metrics version of `get_payload_bodies_by_range_v2`
855    pub async fn get_payload_bodies_by_range_v2_metered(
856        &self,
857        start: BlockNumber,
858        count: u64,
859    ) -> EngineApiResult<ExecutionPayloadBodiesV2> {
860        let start_time = Instant::now();
861        let res = Self::get_payload_bodies_by_range_v2(self, start, count).await;
862        self.inner.metrics.latency.get_payload_bodies_by_range_v2.record(start_time.elapsed());
863        res
864    }
865
866    /// Called to retrieve execution payload bodies by hashes.
867    pub async fn get_payload_bodies_by_hash_with<F, R>(
868        &self,
869        hashes: Vec<BlockHash>,
870        f: F,
871    ) -> EngineApiResult<Vec<Option<R>>>
872    where
873        F: Fn(Provider::Block) -> R + Send + 'static,
874        R: Send + 'static,
875    {
876        let len = hashes.len() as u64;
877        if len > MAX_PAYLOAD_BODIES_LIMIT {
878            return Err(EngineApiError::PayloadRequestTooLarge { len });
879        }
880
881        let (tx, rx) = oneshot::channel();
882        let inner = self.inner.clone();
883
884        self.inner.task_spawner.spawn_blocking_task(async move {
885            let mut result = Vec::with_capacity(hashes.len());
886            for hash in hashes {
887                if tx.is_closed() {
888                    return;
889                }
890
891                let block_result = inner.provider.block(BlockHashOrNumber::Hash(hash));
892                match block_result {
893                    Ok(block) => {
894                        result.push(block.map(&f));
895                    }
896                    Err(err) => {
897                        let _ = tx.send(Err(EngineApiError::Internal(Box::new(err))));
898                        return;
899                    }
900                }
901            }
902            tx.send(Ok(result)).ok();
903        });
904
905        rx.await.map_err(|err| EngineApiError::Internal(Box::new(err)))?
906    }
907
908    /// Returns payload bodies and their timestamps from the same block read.
909    pub async fn get_payload_bodies_by_hash_with_timestamps(
910        &self,
911        hashes: Vec<BlockHash>,
912        include_bal: bool,
913    ) -> EngineApiResult<Vec<Option<(u64, ExecutionPayloadBodyV2)>>> {
914        let bodies =
915            self.get_payload_bodies_by_hash_with(hashes, Self::payload_body_with_timestamp).await?;
916        self.attach_payload_body_bals(bodies, include_bal).await
917    }
918
919    /// Records SSZ body requests in the corresponding JSON-RPC V1/V2 latency histogram.
920    pub async fn get_payload_bodies_by_hash_with_timestamps_metered(
921        &self,
922        hashes: Vec<BlockHash>,
923        include_bal: bool,
924    ) -> EngineApiResult<Vec<Option<(u64, ExecutionPayloadBodyV2)>>> {
925        let start = Instant::now();
926        let result = self.get_payload_bodies_by_hash_with_timestamps(hashes, include_bal).await;
927        let latency = &self.inner.metrics.latency;
928        if include_bal {
929            latency.get_payload_bodies_by_hash_v2.record(start.elapsed());
930        } else {
931            latency.get_payload_bodies_by_hash_v1.record(start.elapsed());
932        }
933        result
934    }
935
936    fn payload_body_with_timestamp(
937        block: Provider::Block,
938    ) -> (BlockHash, u64, ExecutionPayloadBodyV2) {
939        (
940            block.header().hash_slow(),
941            block.header().timestamp(),
942            ExecutionPayloadBodyV2 {
943                transactions: block.body().encoded_2718_transactions(),
944                withdrawals: block.body().withdrawals().cloned().map(Withdrawals::into_inner),
945                block_access_list: None,
946            },
947        )
948    }
949
950    async fn attach_payload_body_bals(
951        &self,
952        mut bodies: Vec<Option<(BlockHash, u64, ExecutionPayloadBodyV2)>>,
953        include_bal: bool,
954    ) -> EngineApiResult<Vec<Option<(u64, ExecutionPayloadBodyV2)>>> {
955        if include_bal {
956            let hashes = bodies.iter().flatten().map(|(hash, _, _)| *hash).collect();
957            let bals = self.get_block_access_lists_by_hashes(hashes).await?;
958            for ((_, _, body), bal) in bodies.iter_mut().flatten().zip(bals) {
959                body.block_access_list = bal;
960            }
961        }
962        Ok(bodies
963            .into_iter()
964            .map(|body| body.map(|(_, timestamp, body)| (timestamp, body)))
965            .collect())
966    }
967
968    async fn get_block_access_lists_by_hashes(
969        &self,
970        hashes: Vec<BlockHash>,
971    ) -> EngineApiResult<Vec<Option<Bytes>>> {
972        let len = hashes.len() as u64;
973        if len > MAX_PAYLOAD_BODIES_LIMIT {
974            return Err(EngineApiError::PayloadRequestTooLarge { len });
975        }
976
977        let (tx, rx) = oneshot::channel();
978        let inner = self.inner.clone();
979
980        self.inner.task_spawner.spawn_blocking_task(async move {
981            if tx.is_closed() {
982                return;
983            }
984
985            tx.send(
986                inner
987                    .provider
988                    .get_bals_by_hashes(&hashes)
989                    .map_err(|err| EngineApiError::Internal(Box::new(err))),
990            )
991            .ok();
992        });
993
994        rx.await.map_err(|err| EngineApiError::Internal(Box::new(err)))?
995    }
996
997    /// Called to retrieve execution payload bodies by hashes.
998    pub async fn get_payload_bodies_by_hash_v1(
999        &self,
1000        hashes: Vec<BlockHash>,
1001    ) -> EngineApiResult<ExecutionPayloadBodiesV1> {
1002        self.get_payload_bodies_by_hash_with(hashes, |block| ExecutionPayloadBodyV1 {
1003            transactions: block.body().encoded_2718_transactions(),
1004            withdrawals: block.body().withdrawals().cloned().map(Withdrawals::into_inner),
1005        })
1006        .await
1007    }
1008
1009    /// Metrics version of `get_payload_bodies_by_hash_v1`
1010    pub async fn get_payload_bodies_by_hash_v1_metered(
1011        &self,
1012        hashes: Vec<BlockHash>,
1013    ) -> EngineApiResult<ExecutionPayloadBodiesV1> {
1014        let start = Instant::now();
1015        let res = Self::get_payload_bodies_by_hash_v1(self, hashes).await;
1016        self.inner.metrics.latency.get_payload_bodies_by_hash_v1.record(start.elapsed());
1017        res
1018    }
1019
1020    /// Called to retrieve execution payload bodies by hashes (V2).
1021    ///
1022    /// V2 includes the `block_access_list` field for EIP-7928 BAL support.
1023    pub async fn get_payload_bodies_by_hash_v2(
1024        &self,
1025        hashes: Vec<BlockHash>,
1026    ) -> EngineApiResult<ExecutionPayloadBodiesV2> {
1027        let payload_bodies =
1028            self.get_payload_bodies_by_hash_with(hashes.clone(), |block| ExecutionPayloadBodyV2 {
1029                transactions: block.body().encoded_2718_transactions(),
1030                withdrawals: block.body().withdrawals().cloned().map(Withdrawals::into_inner),
1031                block_access_list: None,
1032            });
1033        let block_access_lists = self.get_block_access_lists_by_hashes(hashes);
1034        let (mut payload_bodies, block_access_lists) =
1035            tokio::try_join!(payload_bodies, block_access_lists)?;
1036
1037        for (payload_body, block_access_list) in payload_bodies.iter_mut().zip(block_access_lists) {
1038            if let Some(payload_body) = payload_body {
1039                payload_body.block_access_list = block_access_list;
1040            }
1041        }
1042
1043        Ok(payload_bodies)
1044    }
1045
1046    /// Metrics version of `get_payload_bodies_by_hash_v2`
1047    pub async fn get_payload_bodies_by_hash_v2_metered(
1048        &self,
1049        hashes: Vec<BlockHash>,
1050    ) -> EngineApiResult<ExecutionPayloadBodiesV2> {
1051        let start = Instant::now();
1052        let res = Self::get_payload_bodies_by_hash_v2(self, hashes).await;
1053        self.inner.metrics.latency.get_payload_bodies_by_hash_v2.record(start.elapsed());
1054        res
1055    }
1056
1057    /// Validates the `engine_forkchoiceUpdated` payload attributes and executes the forkchoice
1058    /// update.
1059    ///
1060    /// The payload attributes will be validated according to the engine API rules for the given
1061    /// message version:
1062    /// * If the version is [`EngineApiMessageVersion::V1`], then the payload attributes will be
1063    ///   validated according to the Paris rules.
1064    /// * If the version is [`EngineApiMessageVersion::V2`], then the payload attributes will be
1065    ///   validated according to the Shanghai rules, as well as the validity changes from cancun:
1066    ///   <https://github.com/ethereum/execution-apis/blob/584905270d8ad665718058060267061ecfd79ca5/src/engine/cancun.md#update-the-methods-of-previous-forks>
1067    ///
1068    /// * If the version above [`EngineApiMessageVersion::V3`], then the payload attributes will be
1069    ///   validated according to the Cancun rules.
1070    async fn validate_and_execute_forkchoice(
1071        &self,
1072        version: EngineApiMessageVersion,
1073        state: ForkchoiceState,
1074        payload_attrs: Option<EngineT::PayloadAttributes>,
1075    ) -> EngineApiResult<ForkchoiceUpdated> {
1076        if let Some(ref attrs) = payload_attrs {
1077            let attr_validation_res =
1078                self.inner.validator.ensure_well_formed_attributes(version, attrs);
1079
1080            // From the engine API spec:
1081            //
1082            // Client software MUST ensure that payloadAttributes.timestamp is greater than
1083            // timestamp of a block referenced by forkchoiceState.headBlockHash. If this condition
1084            // isn't held client software MUST respond with -38003: Invalid payload attributes and
1085            // MUST NOT begin a payload build process. In such an event, the forkchoiceState
1086            // update MUST NOT be rolled back.
1087            //
1088            // NOTE: This also applies to cancun/shanghai-specific payload attributes.
1089            if let Err(err) = attr_validation_res {
1090                let fcu_res = self.inner.beacon_consensus.fork_choice_updated(state, None).await?;
1091                if fcu_res.is_invalid() || fcu_res.payload_status.is_syncing() {
1092                    return Ok(fcu_res)
1093                }
1094                return Err(err.into())
1095            }
1096        }
1097
1098        Ok(self.inner.beacon_consensus.fork_choice_updated(state, payload_attrs).await?)
1099    }
1100
1101    /// Returns reference to supported capabilities.
1102    pub fn capabilities(&self) -> &EngineCapabilities {
1103        &self.inner.capabilities
1104    }
1105
1106    fn has_blobs(&self, versioned_hashes: Vec<B256>) -> EngineApiResult<Vec<bool>> {
1107        if versioned_hashes.len() > MAX_BLOB_LIMIT {
1108            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
1109        }
1110
1111        self.inner
1112            .tx_pool
1113            .has_blobs_for_versioned_hashes(&versioned_hashes)
1114            .map_err(|err| EngineApiError::Internal(Box::new(err)))
1115    }
1116
1117    /// Metered version of `has_blobs`.
1118    pub fn has_blobs_metered(&self, versioned_hashes: Vec<B256>) -> EngineApiResult<Vec<bool>> {
1119        let start = Instant::now();
1120        let res = Self::has_blobs(self, versioned_hashes);
1121        self.inner.metrics.latency.has_blobs.record(start.elapsed());
1122        res
1123    }
1124
1125    fn get_blobs_v1(
1126        &self,
1127        versioned_hashes: Vec<B256>,
1128    ) -> EngineApiResult<Vec<Option<BlobAndProofV1>>> {
1129        // Only allow this method before Osaka fork
1130        let current_timestamp =
1131            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
1132        if self.inner.chain_spec.is_osaka_active_at_timestamp(current_timestamp) {
1133            return Err(EngineApiError::EngineObjectValidationError(
1134                reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
1135            ));
1136        }
1137
1138        if versioned_hashes.len() > MAX_BLOB_LIMIT {
1139            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
1140        }
1141
1142        self.inner
1143            .tx_pool
1144            .get_blobs_for_versioned_hashes_v1(&versioned_hashes)
1145            .map_err(|err| EngineApiError::Internal(Box::new(err)))
1146    }
1147
1148    /// Metered version of `get_blobs_v1`.
1149    pub fn get_blobs_v1_metered(
1150        &self,
1151        versioned_hashes: Vec<B256>,
1152    ) -> EngineApiResult<Vec<Option<BlobAndProofV1>>> {
1153        let hashes_len = versioned_hashes.len();
1154        let start = Instant::now();
1155        let res = Self::get_blobs_v1(self, versioned_hashes);
1156        self.inner.metrics.latency.get_blobs_v1.record(start.elapsed());
1157
1158        if let Ok(blobs) = &res {
1159            let blobs_found = blobs.iter().flatten().count();
1160            let blobs_missed = hashes_len - blobs_found;
1161
1162            self.inner.metrics.blob_metrics.blob_count.increment(blobs_found as u64);
1163            self.inner.metrics.blob_metrics.blob_misses.increment(blobs_missed as u64);
1164        }
1165
1166        res
1167    }
1168
1169    fn get_blobs_v2(
1170        &self,
1171        versioned_hashes: Vec<B256>,
1172    ) -> EngineApiResult<Option<Vec<BlobAndProofV2>>> {
1173        // Check if Osaka fork is active
1174        let current_timestamp =
1175            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
1176        if !self.inner.chain_spec.is_osaka_active_at_timestamp(current_timestamp) {
1177            return Err(EngineApiError::EngineObjectValidationError(
1178                reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
1179            ));
1180        }
1181
1182        if versioned_hashes.len() > MAX_BLOB_LIMIT {
1183            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
1184        }
1185
1186        self.inner
1187            .tx_pool
1188            .get_blobs_for_versioned_hashes_v2(&versioned_hashes)
1189            .map_err(|err| EngineApiError::Internal(Box::new(err)))
1190    }
1191
1192    fn get_blobs_v3(
1193        &self,
1194        versioned_hashes: Vec<B256>,
1195    ) -> EngineApiResult<Option<Vec<Option<BlobAndProofV2>>>> {
1196        // Check if Osaka fork is active
1197        let current_timestamp =
1198            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
1199        if !self.inner.chain_spec.is_osaka_active_at_timestamp(current_timestamp) {
1200            return Err(EngineApiError::EngineObjectValidationError(
1201                reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
1202            ));
1203        }
1204
1205        if versioned_hashes.len() > MAX_BLOB_LIMIT {
1206            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
1207        }
1208
1209        // Spec requires returning `null` if syncing.
1210        if (*self.inner.is_syncing)() {
1211            return Ok(None)
1212        }
1213
1214        self.inner
1215            .tx_pool
1216            .get_blobs_for_versioned_hashes_v3(&versioned_hashes)
1217            .map(Some)
1218            .map_err(|err| EngineApiError::Internal(Box::new(err)))
1219    }
1220
1221    fn get_blobs_v4(
1222        &self,
1223        versioned_hashes: Vec<B256>,
1224        indices_bitarray: B128,
1225    ) -> EngineApiResult<Option<Vec<Option<BlobCellsAndProofsV1>>>> {
1226        // Engine API bitvectors encode the lowest cell indices in the first byte.
1227        let cell_mask = BlobCellMask::from_bits(u128::from_le_bytes(indices_bitarray.into()));
1228        let current_timestamp =
1229            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
1230        if !self.inner.chain_spec.is_osaka_active_at_timestamp(current_timestamp) {
1231            return Err(EngineApiError::EngineObjectValidationError(
1232                reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
1233            ));
1234        }
1235
1236        if versioned_hashes.len() > MAX_BLOB_LIMIT {
1237            return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() })
1238        }
1239
1240        // Spec requires returning `null` if syncing.
1241        if (*self.inner.is_syncing)() {
1242            return Ok(None)
1243        }
1244
1245        self.inner
1246            .tx_pool
1247            .get_blobs_for_versioned_hashes_v4(&versioned_hashes, cell_mask)
1248            .map(Some)
1249            .map_err(|err| EngineApiError::Internal(Box::new(err)))
1250    }
1251
1252    /// Metered version of `get_blobs_v2`.
1253    pub fn get_blobs_v2_metered(
1254        &self,
1255        versioned_hashes: Vec<B256>,
1256    ) -> EngineApiResult<Option<Vec<BlobAndProofV2>>> {
1257        let hashes_len = versioned_hashes.len();
1258        let start = Instant::now();
1259        let res = Self::get_blobs_v2(self, versioned_hashes);
1260        self.inner.metrics.latency.get_blobs_v2.record(start.elapsed());
1261
1262        if let Ok(blobs) = &res {
1263            let blobs_found = blobs.iter().flatten().count();
1264
1265            self.inner
1266                .metrics
1267                .blob_metrics
1268                .get_blobs_requests_blobs_total
1269                .increment(hashes_len as u64);
1270            self.inner
1271                .metrics
1272                .blob_metrics
1273                .get_blobs_requests_blobs_in_blobpool_total
1274                .increment(blobs_found as u64);
1275
1276            if blobs_found == hashes_len {
1277                self.inner.metrics.blob_metrics.get_blobs_requests_success_total.increment(1);
1278            } else {
1279                self.inner.metrics.blob_metrics.get_blobs_requests_failure_total.increment(1);
1280            }
1281        } else {
1282            self.inner.metrics.blob_metrics.get_blobs_requests_failure_total.increment(1);
1283        }
1284
1285        res
1286    }
1287
1288    /// Metered version of `get_blobs_v3`.
1289    pub fn get_blobs_v3_metered(
1290        &self,
1291        versioned_hashes: Vec<B256>,
1292    ) -> EngineApiResult<Option<Vec<Option<BlobAndProofV2>>>> {
1293        let hashes_len = versioned_hashes.len();
1294        let start = Instant::now();
1295        let res = Self::get_blobs_v3(self, versioned_hashes);
1296        self.inner.metrics.latency.get_blobs_v3.record(start.elapsed());
1297
1298        if let Ok(Some(blobs)) = &res {
1299            let blobs_found = blobs.iter().flatten().count();
1300            let blobs_missed = hashes_len - blobs_found;
1301
1302            self.inner.metrics.blob_metrics.blob_count.increment(blobs_found as u64);
1303            self.inner.metrics.blob_metrics.blob_misses.increment(blobs_missed as u64);
1304        }
1305
1306        res
1307    }
1308
1309    /// Metered version of `get_blobs_v4`.
1310    pub fn get_blobs_v4_metered(
1311        &self,
1312        versioned_hashes: Vec<B256>,
1313        indices_bitarray: B128,
1314    ) -> EngineApiResult<Option<Vec<Option<BlobCellsAndProofsV1>>>> {
1315        let hashes_len = versioned_hashes.len();
1316        let start = Instant::now();
1317        let res = Self::get_blobs_v4(self, versioned_hashes, indices_bitarray);
1318        self.inner.metrics.latency.get_blobs_v4.record(start.elapsed());
1319
1320        if let Ok(Some(blobs)) = &res {
1321            let blobs_found = blobs.iter().flatten().count();
1322            let blobs_missed = hashes_len - blobs_found;
1323
1324            self.inner.metrics.blob_metrics.blob_count.increment(blobs_found as u64);
1325            self.inner.metrics.blob_metrics.blob_misses.increment(blobs_missed as u64);
1326        }
1327
1328        res
1329    }
1330}
1331
1332// This is the concrete ethereum engine API implementation.
1333impl<Provider, EngineT, Pool, Validator, ChainSpec> EngineApiServer<EngineT>
1334    for EngineApi<Provider, EngineT, Pool, Validator, ChainSpec>
1335where
1336    Provider: BlockReader + StateProviderFactory + BalProvider + 'static,
1337    EngineT: EngineTypes<ExecutionData = ExecutionData>,
1338    Pool: TransactionPool + 'static,
1339    Validator: EngineApiValidator<EngineT>,
1340    ChainSpec: EthereumHardforks + Send + Sync + 'static,
1341{
1342    /// Handler for `engine_newPayloadV1`
1343    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_newpayloadv1>
1344    /// Caution: This should not accept the `withdrawals` field
1345    async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> RpcResult<PayloadStatus> {
1346        trace!(target: "rpc::engine", "Serving engine_newPayloadV1");
1347        let payload =
1348            ExecutionData { payload: payload.into(), sidecar: ExecutionPayloadSidecar::none() };
1349        Ok(self.new_payload_v1_metered(payload).await?)
1350    }
1351
1352    /// Handler for `engine_newPayloadV2`
1353    /// See also <https://github.com/ethereum/execution-apis/blob/584905270d8ad665718058060267061ecfd79ca5/src/engine/shanghai.md#engine_newpayloadv2>
1354    async fn new_payload_v2(&self, payload: ExecutionPayloadInputV2) -> RpcResult<PayloadStatus> {
1355        trace!(target: "rpc::engine", "Serving engine_newPayloadV2");
1356        let payload = ExecutionData {
1357            payload: payload.into_payload(),
1358            sidecar: ExecutionPayloadSidecar::none(),
1359        };
1360
1361        Ok(self.new_payload_v2_metered(payload).await?)
1362    }
1363
1364    /// Handler for `engine_newPayloadV3`
1365    /// See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_newpayloadv3>
1366    async fn new_payload_v3(
1367        &self,
1368        payload: ExecutionPayloadV3,
1369        versioned_hashes: Vec<B256>,
1370        parent_beacon_block_root: B256,
1371    ) -> RpcResult<PayloadStatus> {
1372        trace!(target: "rpc::engine", "Serving engine_newPayloadV3");
1373        let payload = ExecutionData {
1374            payload: payload.into(),
1375            sidecar: ExecutionPayloadSidecar::v3(CancunPayloadFields {
1376                versioned_hashes,
1377                parent_beacon_block_root,
1378            }),
1379        };
1380
1381        self.new_payload_v3_metered(payload).await
1382    }
1383
1384    /// Handler for `engine_newPayloadV4`
1385    /// See also <https://github.com/ethereum/execution-apis/blob/03911ffc053b8b806123f1fc237184b0092a485a/src/engine/prague.md#engine_newpayloadv4>
1386    async fn new_payload_v4(
1387        &self,
1388        payload: ExecutionPayloadV3,
1389        versioned_hashes: Vec<B256>,
1390        parent_beacon_block_root: B256,
1391        requests: RequestsOrHash,
1392    ) -> RpcResult<PayloadStatus> {
1393        trace!(target: "rpc::engine", "Serving engine_newPayloadV4");
1394
1395        // Accept requests as a hash only if it is explicitly allowed
1396        if requests.is_hash() && !self.inner.accept_execution_requests_hash {
1397            return Err(EngineApiError::UnexpectedRequestsHash.into());
1398        }
1399
1400        let payload = ExecutionData {
1401            payload: payload.into(),
1402            sidecar: ExecutionPayloadSidecar::v4(
1403                CancunPayloadFields { versioned_hashes, parent_beacon_block_root },
1404                PraguePayloadFields { requests },
1405            ),
1406        };
1407
1408        self.new_payload_v4_metered(payload).await
1409    }
1410
1411    /// Handler for `engine_newPayloadV5`
1412    ///
1413    /// Post Amsterdam payload handler.
1414    ///
1415    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_newpayloadv5>
1416    async fn new_payload_v5(
1417        &self,
1418        payload: ExecutionPayloadV4,
1419        versioned_hashes: Vec<B256>,
1420        parent_beacon_block_root: B256,
1421        requests: RequestsOrHash,
1422    ) -> RpcResult<PayloadStatus> {
1423        trace!(target: "rpc::engine", "Serving engine_newPayloadV5");
1424        // Accept requests as a hash only if it is explicitly allowed.
1425        if requests.is_hash() && !self.inner.accept_execution_requests_hash {
1426            return Err(EngineApiError::UnexpectedRequestsHash.into());
1427        }
1428
1429        let payload = ExecutionData {
1430            payload: payload.into(),
1431            sidecar: ExecutionPayloadSidecar::v4(
1432                CancunPayloadFields { versioned_hashes, parent_beacon_block_root },
1433                PraguePayloadFields { requests },
1434            ),
1435        };
1436
1437        self.new_payload_v5_metered(payload).await
1438    }
1439
1440    /// Handler for `engine_newPayloadV6`.
1441    ///
1442    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#engine_newpayloadv6>
1443    async fn new_payload_v6(
1444        &self,
1445        payload: ExecutionPayloadV4,
1446        versioned_hashes: Vec<B256>,
1447        parent_beacon_block_root: B256,
1448        execution_requests: RequestsOrHash,
1449        inclusion_list_transactions: Vec<Bytes>,
1450    ) -> RpcResult<PayloadStatusV2> {
1451        trace!(target: "rpc::engine", "Serving engine_newPayloadV6");
1452        if execution_requests.is_hash() && !self.inner.accept_execution_requests_hash {
1453            return Err(EngineApiError::UnexpectedRequestsHash.into());
1454        }
1455
1456        let payload = ExecutionData {
1457            payload: payload.into(),
1458            sidecar: ExecutionPayloadSidecar::v6(
1459                CancunPayloadFields { versioned_hashes, parent_beacon_block_root },
1460                PraguePayloadFields { requests: execution_requests },
1461                BogotaPayloadFields { inclusion_list_transactions },
1462            ),
1463        };
1464
1465        // TODO: perform structural validation of the inclusion list transactions and populate
1466        // `inclusion_list_satisfied` for VALID payloads
1467        Ok(self.new_payload_v6_metered(payload).await?)
1468    }
1469
1470    /// Handler for `engine_forkchoiceUpdatedV1`
1471    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_forkchoiceupdatedv1>
1472    ///
1473    /// Caution: This should not accept the `withdrawals` field
1474    async fn fork_choice_updated_v1(
1475        &self,
1476        fork_choice_state: ForkchoiceState,
1477        payload_attributes: Option<EngineT::PayloadAttributes>,
1478    ) -> RpcResult<ForkchoiceUpdated> {
1479        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV1");
1480        Ok(self.fork_choice_updated_v1_metered(fork_choice_state, payload_attributes).await?)
1481    }
1482
1483    /// Handler for `engine_forkchoiceUpdatedV2`
1484    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/shanghai.md#engine_forkchoiceupdatedv2>
1485    async fn fork_choice_updated_v2(
1486        &self,
1487        fork_choice_state: ForkchoiceState,
1488        payload_attributes: Option<EngineT::PayloadAttributes>,
1489    ) -> RpcResult<ForkchoiceUpdated> {
1490        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV2");
1491        Ok(self.fork_choice_updated_v2_metered(fork_choice_state, payload_attributes).await?)
1492    }
1493
1494    /// Handler for `engine_forkchoiceUpdatedV3`
1495    ///
1496    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#engine_forkchoiceupdatedv3>
1497    async fn fork_choice_updated_v3(
1498        &self,
1499        fork_choice_state: ForkchoiceState,
1500        payload_attributes: Option<EngineT::PayloadAttributes>,
1501    ) -> RpcResult<ForkchoiceUpdated> {
1502        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV3");
1503        Ok(self.fork_choice_updated_v3_metered(fork_choice_state, payload_attributes).await?)
1504    }
1505
1506    /// Handler for `engine_forkchoiceUpdatedV4`
1507    ///
1508    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_forkchoiceupdatedv4>
1509    async fn fork_choice_updated_v4(
1510        &self,
1511        fork_choice_state: ForkchoiceState,
1512        payload_attributes: Option<EngineT::PayloadAttributes>,
1513        custody_columns: Option<B128>,
1514    ) -> RpcResult<ForkchoiceUpdated> {
1515        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV4");
1516        Ok(self
1517            .fork_choice_updated_v4_metered(fork_choice_state, payload_attributes, custody_columns)
1518            .await?)
1519    }
1520
1521    /// Handler for `engine_forkchoiceUpdatedV5`.
1522    ///
1523    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#engine_forkchoiceupdatedv5>
1524    async fn fork_choice_updated_v5(
1525        &self,
1526        fork_choice_state: ForkchoiceState,
1527        payload_attributes: Option<EngineT::PayloadAttributes>,
1528        custody_columns: Option<B128>,
1529    ) -> RpcResult<ForkchoiceUpdatedResponseV2> {
1530        trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV5");
1531        Ok(self
1532            .fork_choice_updated_v5_metered(fork_choice_state, payload_attributes, custody_columns)
1533            .await?)
1534    }
1535
1536    /// Handler for `engine_getPayloadV1`
1537    ///
1538    /// Returns the most recent version of the payload that is available in the corresponding
1539    /// payload build process at the time of receiving this call.
1540    ///
1541    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_getPayloadV1>
1542    ///
1543    /// Caution: This should not return the `withdrawals` field
1544    ///
1545    /// Note:
1546    /// > Provider software MAY stop the corresponding build process after serving this call.
1547    async fn get_payload_v1(
1548        &self,
1549        payload_id: PayloadId,
1550    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV1> {
1551        trace!(target: "rpc::engine", "Serving engine_getPayloadV1");
1552        Ok(self.get_payload_v1_metered(payload_id).await?)
1553    }
1554
1555    /// Handler for `engine_getPayloadV2`
1556    ///
1557    /// Returns the most recent version of the payload that is available in the corresponding
1558    /// payload build process at the time of receiving this call.
1559    ///
1560    /// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/shanghai.md#engine_getpayloadv2>
1561    ///
1562    /// Note:
1563    /// > Provider software MAY stop the corresponding build process after serving this call.
1564    async fn get_payload_v2(
1565        &self,
1566        payload_id: PayloadId,
1567    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV2> {
1568        debug!(target: "rpc::engine", id = %payload_id, "Serving engine_getPayloadV2");
1569        Ok(self.get_payload_v2_metered(payload_id).await?)
1570    }
1571
1572    /// Handler for `engine_getPayloadV3`
1573    ///
1574    /// Returns the most recent version of the payload that is available in the corresponding
1575    /// payload build process at the time of receiving this call.
1576    ///
1577    /// See also <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#engine_getpayloadv3>
1578    ///
1579    /// Note:
1580    /// > Provider software MAY stop the corresponding build process after serving this call.
1581    async fn get_payload_v3(
1582        &self,
1583        payload_id: PayloadId,
1584    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV3> {
1585        trace!(target: "rpc::engine", "Serving engine_getPayloadV3");
1586        Ok(self.get_payload_v3_metered(payload_id).await?)
1587    }
1588
1589    /// Handler for `engine_getPayloadV4`
1590    ///
1591    /// Returns the most recent version of the payload that is available in the corresponding
1592    /// payload build process at the time of receiving this call.
1593    ///
1594    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md#engine_getpayloadv4>
1595    ///
1596    /// Note:
1597    /// > Provider software MAY stop the corresponding build process after serving this call.
1598    async fn get_payload_v4(
1599        &self,
1600        payload_id: PayloadId,
1601    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV4> {
1602        trace!(target: "rpc::engine", "Serving engine_getPayloadV4");
1603        Ok(self.get_payload_v4_metered(payload_id).await?)
1604    }
1605
1606    /// Handler for `engine_getPayloadV5`
1607    ///
1608    /// Returns the most recent version of the payload that is available in the corresponding
1609    /// payload build process at the time of receiving this call.
1610    ///
1611    /// See also <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#engine_getpayloadv5>
1612    ///
1613    /// Note:
1614    /// > Provider software MAY stop the corresponding build process after serving this call.
1615    async fn get_payload_v5(
1616        &self,
1617        payload_id: PayloadId,
1618    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV5> {
1619        trace!(target: "rpc::engine", "Serving engine_getPayloadV5");
1620        Ok(self.get_payload_v5_metered(payload_id).await?)
1621    }
1622
1623    /// Handler for `engine_getPayloadV6`
1624    ///
1625    /// Post-Amsterdam payload handler that includes Block Access Lists (BAL).
1626    ///
1627    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_getpayloadv6>
1628    async fn get_payload_v6(
1629        &self,
1630        payload_id: PayloadId,
1631    ) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV6> {
1632        trace!(target: "rpc::engine", "Serving engine_getPayloadV6");
1633        Ok(self.get_payload_v6_metered(payload_id).await?)
1634    }
1635
1636    /// Handler for `engine_getInclusionListV1`.
1637    ///
1638    /// See also <https://github.com/ethereum/execution-apis/pull/609>.
1639    async fn get_inclusion_list_v1(&self) -> RpcResult<Vec<Bytes>> {
1640        trace!(target: "rpc::engine", "Serving engine_getInclusionListV1");
1641        Ok(self.get_inclusion_list_v1_metered()?)
1642    }
1643
1644    /// Handler for `engine_getPayloadBodiesByHashV1`
1645    /// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#engine_getpayloadbodiesbyhashv1>
1646    async fn get_payload_bodies_by_hash_v1(
1647        &self,
1648        block_hashes: Vec<BlockHash>,
1649    ) -> RpcResult<ExecutionPayloadBodiesV1> {
1650        trace!(target: "rpc::engine", "Serving engine_getPayloadBodiesByHashV1");
1651        Ok(self.get_payload_bodies_by_hash_v1_metered(block_hashes).await?)
1652    }
1653
1654    /// Handler for `engine_getPayloadBodiesByHashV2`
1655    ///
1656    /// V2 includes the `block_access_list` field for EIP-7928 BAL support.
1657    ///
1658    /// See also <https://eips.ethereum.org/EIPS/eip-7928>
1659    async fn get_payload_bodies_by_hash_v2(
1660        &self,
1661        block_hashes: Vec<BlockHash>,
1662    ) -> RpcResult<ExecutionPayloadBodiesV2> {
1663        trace!(target: "rpc::engine", "Serving engine_getPayloadBodiesByHashV2");
1664        Ok(self.get_payload_bodies_by_hash_v2_metered(block_hashes).await?)
1665    }
1666
1667    /// Handler for `engine_getPayloadBodiesByRangeV1`
1668    ///
1669    /// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#engine_getpayloadbodiesbyrangev1>
1670    ///
1671    /// Returns the execution payload bodies by the range starting at `start`, containing `count`
1672    /// blocks.
1673    ///
1674    /// WARNING: This method is associated with the `BeaconBlocksByRange` message in the consensus
1675    /// layer p2p specification, meaning the input should be treated as untrusted or potentially
1676    /// adversarial.
1677    ///
1678    /// Implementers should take care when acting on the input to this method, specifically
1679    /// ensuring that the range is limited properly, and that the range boundaries are computed
1680    /// correctly and without panics.
1681    ///
1682    /// Note: If a block is pre shanghai, `withdrawals` field will be `null`.
1683    async fn get_payload_bodies_by_range_v1(
1684        &self,
1685        start: U64,
1686        count: U64,
1687    ) -> RpcResult<ExecutionPayloadBodiesV1> {
1688        trace!(target: "rpc::engine", "Serving engine_getPayloadBodiesByRangeV1");
1689        Ok(self.get_payload_bodies_by_range_v1_metered(start.to(), count.to()).await?)
1690    }
1691
1692    /// Handler for `engine_getPayloadBodiesByRangeV2`
1693    ///
1694    /// V2 includes the `block_access_list` field for EIP-7928 BAL support.
1695    ///
1696    /// See also <https://eips.ethereum.org/EIPS/eip-7928>
1697    async fn get_payload_bodies_by_range_v2(
1698        &self,
1699        start: U64,
1700        count: U64,
1701    ) -> RpcResult<ExecutionPayloadBodiesV2> {
1702        trace!(target: "rpc::engine", "Serving engine_getPayloadBodiesByRangeV2");
1703        Ok(self.get_payload_bodies_by_range_v2_metered(start.to(), count.to()).await?)
1704    }
1705
1706    /// Handler for `engine_getClientVersionV1`
1707    ///
1708    /// See also <https://github.com/ethereum/execution-apis/blob/03911ffc053b8b806123f1fc237184b0092a485a/src/engine/identification.md>
1709    async fn get_client_version_v1(
1710        &self,
1711        client: ClientVersionV1,
1712    ) -> RpcResult<Vec<ClientVersionV1>> {
1713        trace!(target: "rpc::engine", "Serving engine_getClientVersionV1");
1714        Ok(Self::get_client_version_v1(self, client)?)
1715    }
1716
1717    /// Handler for `engine_exchangeCapabilitiesV1`
1718    /// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/common.md#capabilities>
1719    async fn exchange_capabilities(&self, capabilities: Vec<String>) -> RpcResult<Vec<String>> {
1720        trace!(target: "rpc::engine", "Serving engine_exchangeCapabilities");
1721
1722        let el_caps = self.capabilities();
1723        el_caps.log_capability_mismatches(&capabilities);
1724
1725        Ok(el_caps.list())
1726    }
1727
1728    async fn has_blobs(&self, versioned_hashes: Vec<B256>) -> RpcResult<Vec<bool>> {
1729        trace!(target: "rpc::engine", "Serving engine_hasBlobs");
1730        Ok(self.has_blobs_metered(versioned_hashes)?)
1731    }
1732
1733    async fn get_blobs_v1(
1734        &self,
1735        versioned_hashes: Vec<B256>,
1736    ) -> RpcResult<Vec<Option<BlobAndProofV1>>> {
1737        trace!(target: "rpc::engine", "Serving engine_getBlobsV1");
1738        Ok(self.get_blobs_v1_metered(versioned_hashes)?)
1739    }
1740
1741    async fn get_blobs_v2(
1742        &self,
1743        versioned_hashes: Vec<B256>,
1744    ) -> RpcResult<Option<Vec<BlobAndProofV2>>> {
1745        trace!(target: "rpc::engine", "Serving engine_getBlobsV2");
1746        Ok(self.get_blobs_v2_metered(versioned_hashes)?)
1747    }
1748
1749    async fn get_blobs_v3(
1750        &self,
1751        versioned_hashes: Vec<B256>,
1752    ) -> RpcResult<Option<Vec<Option<BlobAndProofV2>>>> {
1753        trace!(target: "rpc::engine", "Serving engine_getBlobsV3");
1754        Ok(self.get_blobs_v3_metered(versioned_hashes)?)
1755    }
1756
1757    async fn get_blobs_v4(
1758        &self,
1759        versioned_hashes: Vec<B256>,
1760        indices_bitarray: B128,
1761    ) -> RpcResult<Option<Vec<Option<BlobCellsAndProofsV1>>>> {
1762        trace!(target: "rpc::engine", "Serving engine_getBlobsV4");
1763        Ok(self.get_blobs_v4_metered(versioned_hashes, indices_bitarray)?)
1764    }
1765}
1766
1767impl<Provider, EngineT, Pool, Validator, ChainSpec> IntoEngineApiRpcModule
1768    for EngineApi<Provider, EngineT, Pool, Validator, ChainSpec>
1769where
1770    EngineT: EngineTypes,
1771    Self: EngineApiServer<EngineT>,
1772{
1773    fn into_rpc_module(self) -> RpcModule<()> {
1774        EngineApiServer::<EngineT>::into_rpc(self).remove_context()
1775    }
1776}
1777
1778impl<Provider, PayloadT, Pool, Validator, ChainSpec> std::fmt::Debug
1779    for EngineApi<Provider, PayloadT, Pool, Validator, ChainSpec>
1780where
1781    PayloadT: PayloadTypes,
1782{
1783    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1784        f.debug_struct("EngineApi").finish_non_exhaustive()
1785    }
1786}
1787
1788impl<Provider, PayloadT, Pool, Validator, ChainSpec> Clone
1789    for EngineApi<Provider, PayloadT, Pool, Validator, ChainSpec>
1790where
1791    PayloadT: PayloadTypes,
1792{
1793    fn clone(&self) -> Self {
1794        Self { inner: Arc::clone(&self.inner) }
1795    }
1796}
1797
1798/// The container type for the engine API internals.
1799struct EngineApiInner<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSpec> {
1800    /// The provider to interact with the chain.
1801    provider: Provider,
1802    /// Consensus configuration
1803    chain_spec: Arc<ChainSpec>,
1804    /// The channel to send messages to the beacon consensus engine.
1805    beacon_consensus: ConsensusEngineHandle<PayloadT>,
1806    /// The type that can communicate with the payload service to retrieve payloads.
1807    payload_store: PayloadStore<PayloadT>,
1808    /// For spawning and executing async tasks
1809    task_spawner: Runtime,
1810    /// The latency and response type metrics for engine api calls
1811    metrics: EngineApiMetrics,
1812    /// Identification of the execution client used by the consensus client
1813    client: ClientVersionV1,
1814    /// The list of all supported Engine capabilities available over the engine endpoint.
1815    capabilities: EngineCapabilities,
1816    /// Transaction pool.
1817    tx_pool: Pool,
1818    /// Engine validator.
1819    validator: Validator,
1820    accept_execution_requests_hash: bool,
1821    /// Shared blob cell custody bitmap.
1822    cell_custody: CellCustody,
1823    /// Returns `true` if the node is currently syncing.
1824    is_syncing: Arc<dyn Fn() -> bool + Send + Sync>,
1825}
1826
1827#[cfg(test)]
1828mod tests {
1829    use super::*;
1830    use crate::error::UNSUPPORTED_FORK_CODE;
1831    use alloy_eips::{eip7685::Requests, Encodable2718, NumHash};
1832    use alloy_primitives::{Address, Bytes, B256};
1833    use alloy_rpc_types_engine::{
1834        ClientCode, ClientVersionV1, ExecutionPayloadV2, PayloadAttributes, PayloadStatusEnum,
1835    };
1836    use assert_matches::assert_matches;
1837    use jsonrpsee_types::ErrorObject;
1838    use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET};
1839    use reth_engine_primitives::{BeaconEngineMessage, OnForkChoiceUpdated};
1840    use reth_ethereum_engine_primitives::EthEngineTypes;
1841    use reth_ethereum_primitives::{Block, TransactionSigned};
1842    use reth_network_api::{
1843        noop::NoopNetwork, EthProtocolInfo, NetworkError, NetworkInfo, NetworkStatus,
1844    };
1845    use reth_node_ethereum::EthereumEngineValidator;
1846    use reth_payload_builder::test_utils::spawn_test_payload_service;
1847    use reth_payload_primitives::EngineObjectValidationError;
1848    use reth_primitives_traits::SignedTransaction;
1849    use reth_provider::{test_utils::MockEthProvider, BalStoreHandle, InMemoryBalStore, RawBal};
1850    use reth_tasks::Runtime;
1851    use reth_transaction_pool::{
1852        blobstore::InMemoryBlobStore,
1853        noop::NoopTransactionPool,
1854        test_utils::{OkValidator, TransactionBuilder},
1855        CoinbaseTipOrdering, EthPooledTransaction, Pool, PoolTransaction, TransactionOrigin,
1856    };
1857    use std::time::Duration;
1858    use tokio::sync::mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver};
1859
1860    type EthTestPool = Pool<
1861        OkValidator<EthPooledTransaction>,
1862        CoinbaseTipOrdering<EthPooledTransaction>,
1863        InMemoryBlobStore,
1864    >;
1865
1866    fn eth_test_pool() -> EthTestPool {
1867        Pool::new(
1868            OkValidator::default(),
1869            CoinbaseTipOrdering::default(),
1870            InMemoryBlobStore::default(),
1871            Default::default(),
1872        )
1873    }
1874
1875    fn pooled_transaction(transaction: TransactionSigned) -> EthPooledTransaction {
1876        let transaction = transaction.try_into_recovered().unwrap();
1877        let encoded_length = transaction.encode_2718_len();
1878        EthPooledTransaction::new(transaction, encoded_length)
1879    }
1880
1881    fn setup_engine_api() -> (
1882        EngineApiTestHandle,
1883        EngineApi<
1884            Arc<MockEthProvider>,
1885            EthEngineTypes,
1886            NoopTransactionPool,
1887            EthereumEngineValidator,
1888            ChainSpec,
1889        >,
1890    ) {
1891        setup_engine_api_with_pool(NoopTransactionPool::default())
1892    }
1893
1894    fn setup_engine_api_with_pool<Pool>(
1895        tx_pool: Pool,
1896    ) -> (
1897        EngineApiTestHandle,
1898        EngineApi<Arc<MockEthProvider>, EthEngineTypes, Pool, EthereumEngineValidator, ChainSpec>,
1899    )
1900    where
1901        Pool: TransactionPool + 'static,
1902    {
1903        setup_engine_api_with_provider(tx_pool, MockEthProvider::default())
1904    }
1905
1906    fn setup_engine_api_with_provider<Pool>(
1907        tx_pool: Pool,
1908        provider: MockEthProvider,
1909    ) -> (
1910        EngineApiTestHandle,
1911        EngineApi<Arc<MockEthProvider>, EthEngineTypes, Pool, EthereumEngineValidator, ChainSpec>,
1912    )
1913    where
1914        Pool: TransactionPool + 'static,
1915    {
1916        let client = ClientVersionV1 {
1917            code: ClientCode::RH,
1918            name: "Reth".to_string(),
1919            version: "v0.2.0-beta.5".to_string(),
1920            commit: "defa64b2".to_string(),
1921        };
1922
1923        let chain_spec: Arc<ChainSpec> = MAINNET.clone();
1924        let provider = Arc::new(provider);
1925        let payload_store = spawn_test_payload_service();
1926        let (to_engine, engine_rx) = unbounded_channel();
1927        let task_executor = Runtime::test();
1928        let api = EngineApi::new(
1929            provider.clone(),
1930            chain_spec.clone(),
1931            ConsensusEngineHandle::new(to_engine),
1932            payload_store.into(),
1933            tx_pool,
1934            task_executor,
1935            client,
1936            EngineCapabilities::default(),
1937            EthereumEngineValidator::new(chain_spec.clone()),
1938            false,
1939            NoopNetwork::default(),
1940        );
1941        let handle = EngineApiTestHandle { chain_spec, provider, from_api: engine_rx };
1942        (handle, api)
1943    }
1944
1945    #[tokio::test]
1946    async fn engine_client_version_v1() {
1947        let client = ClientVersionV1 {
1948            code: ClientCode::RH,
1949            name: "Reth".to_string(),
1950            version: "v0.2.0-beta.5".to_string(),
1951            commit: "defa64b2".to_string(),
1952        };
1953        let (_, api) = setup_engine_api();
1954        let res = api.get_client_version_v1(client.clone());
1955        assert_eq!(res.unwrap(), vec![client]);
1956    }
1957
1958    #[tokio::test]
1959    async fn get_inclusion_list_v1_stops_at_size_limit() {
1960        let pool = eth_test_pool();
1961        let first = pooled_transaction(
1962            TransactionBuilder::default()
1963                .max_fee_per_gas(3_000_000_000u128)
1964                .input(vec![0; 4_200])
1965                .into_legacy(),
1966        );
1967        let second = pooled_transaction(
1968            TransactionBuilder::default()
1969                .max_fee_per_gas(2_000_000_000u128)
1970                .input(vec![0; 4_200])
1971                .into_legacy(),
1972        );
1973        let third = pooled_transaction(
1974            TransactionBuilder::default().max_fee_per_gas(1_000_000_000u128).into_legacy(),
1975        );
1976        let expected = first.encoded_2718_consensus();
1977
1978        pool.add_transaction(TransactionOrigin::External, first).await.unwrap();
1979        pool.add_transaction(TransactionOrigin::External, second).await.unwrap();
1980        pool.add_transaction(TransactionOrigin::External, third).await.unwrap();
1981        let (_, api) = setup_engine_api_with_pool(pool);
1982
1983        let res = EngineApiServer::get_inclusion_list_v1(&api).await.unwrap();
1984        assert_eq!(res, vec![expected]);
1985        assert!(
1986            res.iter().map(|tx| tx.len()).sum::<usize>() <= MAX_BYTES_PER_INCLUSION_LIST as usize
1987        );
1988    }
1989
1990    /// Signs candidate transactions because signature lengths affect the final encoded size.
1991    fn inclusion_list_transaction(size: usize, typed: bool, fee: u128) -> EthPooledTransaction {
1992        for input_size in size.saturating_sub(256)..size {
1993            let builder = TransactionBuilder::default()
1994                .signer(B256::with_last_byte(if typed { 2 } else { 1 }))
1995                .max_fee_per_gas(fee)
1996                .max_priority_fee_per_gas(fee)
1997                .input(vec![0; input_size]);
1998            let transaction = if typed { builder.into_eip1559() } else { builder.into_legacy() };
1999            if transaction.encode_2718_len() == size {
2000                return pooled_transaction(transaction)
2001            }
2002        }
2003        panic!("could not construct a transaction of {size} bytes")
2004    }
2005
2006    #[tokio::test]
2007    async fn get_inclusion_list_v1_transaction_byte_boundaries() {
2008        let limit = MAX_BYTES_PER_INCLUSION_LIST as usize;
2009        for typed in [false, true] {
2010            for size in [limit - 3, limit, limit + 1] {
2011                let pool = eth_test_pool();
2012                let transaction = inclusion_list_transaction(size, typed, 3_000_000_000);
2013                let encoded = transaction.encoded_2718_consensus();
2014                assert_eq!(encoded.len(), size);
2015                pool.add_transaction(TransactionOrigin::External, transaction).await.unwrap();
2016                let (_, api) = setup_engine_api_with_pool(pool);
2017
2018                let res = EngineApiServer::get_inclusion_list_v1(&api).await.unwrap();
2019                let expected = if size <= limit { vec![encoded] } else { vec![] };
2020                assert_eq!(res, expected, "typed={typed}, size={size}");
2021                assert!(res.iter().all(|tx| !tx.is_empty()));
2022            }
2023        }
2024    }
2025
2026    #[tokio::test]
2027    async fn get_inclusion_list_v1_cumulative_transaction_byte_boundaries() {
2028        let limit = MAX_BYTES_PER_INCLUSION_LIST as usize;
2029        for extra in [0, 1] {
2030            let pool = eth_test_pool();
2031            let first = inclusion_list_transaction(limit / 2, false, 3_000_000_000);
2032            let second = inclusion_list_transaction(limit / 2 + extra, true, 2_000_000_000);
2033            let mut expected =
2034                vec![first.encoded_2718_consensus(), second.encoded_2718_consensus()];
2035            assert_eq!(expected.iter().map(|tx| tx.len()).sum::<usize>(), limit + extra);
2036            if extra != 0 {
2037                expected.pop();
2038            }
2039            pool.add_transaction(TransactionOrigin::External, first).await.unwrap();
2040            pool.add_transaction(TransactionOrigin::External, second).await.unwrap();
2041            let (_, api) = setup_engine_api_with_pool(pool);
2042
2043            let res = EngineApiServer::get_inclusion_list_v1(&api).await.unwrap();
2044            assert_eq!(res, expected);
2045            assert!(res.iter().all(|tx| !tx.is_empty()));
2046        }
2047    }
2048
2049    #[tokio::test]
2050    async fn get_inclusion_list_v1_excludes_blob_transactions() {
2051        let pool = eth_test_pool();
2052        let blob = pooled_transaction(
2053            TransactionBuilder::default()
2054                .max_fee_per_gas(2_000_000_000u128)
2055                .max_priority_fee_per_gas(1_000_000_000u128)
2056                .into_eip4844(),
2057        );
2058        let non_blob = pooled_transaction(
2059            TransactionBuilder::default()
2060                .max_fee_per_gas(1_000_000_000u128)
2061                .max_priority_fee_per_gas(1_000_000_000u128)
2062                .into_eip1559(),
2063        );
2064        let expected = non_blob.encoded_2718_consensus();
2065
2066        pool.add_transaction(TransactionOrigin::External, blob).await.unwrap();
2067        pool.add_transaction(TransactionOrigin::External, non_blob).await.unwrap();
2068        let (_, api) = setup_engine_api_with_pool(pool);
2069
2070        let res = EngineApiServer::get_inclusion_list_v1(&api).await.unwrap();
2071        assert_eq!(res, vec![expected]);
2072    }
2073
2074    #[tokio::test]
2075    async fn has_blobs_returns_ordered_availability() {
2076        let (_, api) = setup_engine_api();
2077
2078        let res = api.has_blobs_metered(vec![B256::ZERO, B256::with_last_byte(1)]).unwrap();
2079        assert_eq!(res, vec![false, false]);
2080    }
2081
2082    #[tokio::test]
2083    async fn has_blobs_rejects_large_requests() {
2084        let (_, api) = setup_engine_api();
2085
2086        let res = api.has_blobs_metered(vec![B256::ZERO; MAX_BLOB_LIMIT + 1]);
2087        assert_matches!(
2088            res,
2089            Err(EngineApiError::BlobRequestTooLarge { len }) if len == MAX_BLOB_LIMIT + 1
2090        );
2091    }
2092
2093    #[tokio::test]
2094    async fn payload_bodies_with_timestamps_preserve_missing_entries_and_head_truncation() {
2095        let mut provider = MockEthProvider::default();
2096        provider.bal_store = BalStoreHandle::new(InMemoryBalStore::default());
2097        let (handle, api) =
2098            setup_engine_api_with_provider(NoopTransactionPool::default(), provider);
2099        let mut block = Block::default();
2100        block.header.number = 1;
2101        block.header.timestamp = 123;
2102        let hash = block.header.hash_slow();
2103        handle.provider.add_block(hash, block);
2104        let raw_bal = Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]);
2105        handle
2106            .provider
2107            .bal_store
2108            .insert(NumHash::new(1, hash), RawBal::new(raw_bal.clone()))
2109            .unwrap();
2110        for include_bal in [false, true] {
2111            let by_hash = api
2112                .get_payload_bodies_by_hash_with_timestamps(
2113                    vec![B256::ZERO, hash, hash],
2114                    include_bal,
2115                )
2116                .await
2117                .unwrap();
2118            assert_eq!(by_hash.len(), 3);
2119            assert!(by_hash[0].is_none());
2120            assert_eq!(by_hash[1], by_hash[2]);
2121            let (timestamp, body) = by_hash[1].as_ref().unwrap();
2122            assert_eq!(*timestamp, 123);
2123            assert_eq!(body.block_access_list, include_bal.then(|| raw_bal.clone()));
2124            let by_range =
2125                api.get_payload_bodies_by_range_with_timestamps(1, 3, include_bal).await.unwrap();
2126            assert_eq!(by_range, vec![by_hash[1].clone()]);
2127            assert!(api
2128                .get_payload_bodies_by_range_with_timestamps(2, 3, include_bal)
2129                .await
2130                .unwrap()
2131                .is_empty());
2132        }
2133    }
2134
2135    #[tokio::test]
2136    async fn get_payload_bodies_by_hash_v2_returns_block_access_list_from_store() {
2137        let bal_store = BalStoreHandle::new(InMemoryBalStore::default());
2138        let mut provider = MockEthProvider::default();
2139        provider.bal_store = bal_store.clone();
2140        let provider = Arc::new(provider);
2141
2142        let client = ClientVersionV1 {
2143            code: ClientCode::RH,
2144            name: "Reth".to_string(),
2145            version: "v0.2.0-beta.5".to_string(),
2146            commit: "defa64b2".to_string(),
2147        };
2148        let chain_spec: Arc<ChainSpec> = MAINNET.clone();
2149        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
2150        let (to_engine, _engine_rx) = unbounded_channel();
2151        let api = EngineApi::new(
2152            provider.clone(),
2153            chain_spec.clone(),
2154            ConsensusEngineHandle::new(to_engine),
2155            payload_store.into(),
2156            NoopTransactionPool::default(),
2157            Runtime::test(),
2158            client,
2159            EngineCapabilities::default(),
2160            EthereumEngineValidator::new(chain_spec),
2161            false,
2162            NoopNetwork::default(),
2163        );
2164
2165        let mut block = Block::default();
2166        block.header.number = 1;
2167        let block_hash = block.header.hash_slow();
2168        provider.add_block(block_hash, block);
2169
2170        let mut block_without_bal = Block::default();
2171        block_without_bal.header.number = 2;
2172        let block_without_bal_hash = block_without_bal.header.hash_slow();
2173        provider.add_block(block_without_bal_hash, block_without_bal);
2174
2175        let raw_bal = Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]);
2176        bal_store.insert(NumHash::new(1, block_hash), RawBal::new(raw_bal.clone())).unwrap();
2177
2178        let missing_hash = B256::with_last_byte(3);
2179        let response = api
2180            .get_payload_bodies_by_hash_v2(vec![block_hash, block_without_bal_hash, missing_hash])
2181            .await
2182            .unwrap();
2183
2184        assert_eq!(response.len(), 3);
2185        assert_eq!(response[0].as_ref().unwrap().block_access_list, Some(raw_bal));
2186        assert_eq!(response[1].as_ref().unwrap().block_access_list, None);
2187        assert!(response[2].is_none());
2188    }
2189
2190    #[tokio::test]
2191    async fn get_payload_bodies_by_range_v2_returns_block_access_lists_from_store() {
2192        let bal_store = BalStoreHandle::new(InMemoryBalStore::default());
2193        let mut provider = MockEthProvider::default();
2194        provider.bal_store = bal_store.clone();
2195        let provider = Arc::new(provider);
2196
2197        let client = ClientVersionV1 {
2198            code: ClientCode::RH,
2199            name: "Reth".to_string(),
2200            version: "v0.2.0-beta.5".to_string(),
2201            commit: "defa64b2".to_string(),
2202        };
2203        let chain_spec: Arc<ChainSpec> = MAINNET.clone();
2204        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
2205        let (to_engine, _engine_rx) = unbounded_channel();
2206        let api = EngineApi::new(
2207            provider.clone(),
2208            chain_spec.clone(),
2209            ConsensusEngineHandle::new(to_engine),
2210            payload_store.into(),
2211            NoopTransactionPool::default(),
2212            Runtime::test(),
2213            client,
2214            EngineCapabilities::default(),
2215            EthereumEngineValidator::new(chain_spec),
2216            false,
2217            NoopNetwork::default(),
2218        );
2219
2220        let mut block = Block::default();
2221        block.header.number = 1;
2222        let block_hash = block.header.hash_slow();
2223        provider.add_block(block_hash, block);
2224
2225        let mut block_without_bal = Block::default();
2226        block_without_bal.header.number = 3;
2227        let block_without_bal_hash = block_without_bal.header.hash_slow();
2228        provider.add_block(block_without_bal_hash, block_without_bal);
2229
2230        let raw_bal = Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]);
2231        bal_store.insert(NumHash::new(1, block_hash), RawBal::new(raw_bal.clone())).unwrap();
2232
2233        let response = api.get_payload_bodies_by_range_v2(1, 3).await.unwrap();
2234
2235        assert_eq!(response.len(), 3);
2236        assert_eq!(response[0].as_ref().unwrap().block_access_list, Some(raw_bal));
2237        assert!(response[1].is_none());
2238        assert_eq!(response[2].as_ref().unwrap().block_access_list, None);
2239    }
2240
2241    struct EngineApiTestHandle {
2242        #[allow(dead_code)]
2243        chain_spec: Arc<ChainSpec>,
2244        provider: Arc<MockEthProvider>,
2245        from_api: UnboundedReceiver<BeaconEngineMessage<EthEngineTypes>>,
2246    }
2247
2248    #[tokio::test]
2249    async fn forwards_responses_to_consensus_engine() {
2250        let (mut handle, api) = setup_engine_api();
2251
2252        tokio::spawn(async move {
2253            let payload_v1 = ExecutionPayloadV1::from_block_slow(&Block::default());
2254            let execution_data = ExecutionData {
2255                payload: payload_v1.into(),
2256                sidecar: ExecutionPayloadSidecar::none(),
2257            };
2258
2259            api.new_payload_v1(execution_data).await.unwrap();
2260        });
2261        assert_matches!(handle.from_api.recv().await, Some(BeaconEngineMessage::NewPayload { .. }));
2262    }
2263
2264    #[tokio::test]
2265    async fn new_payload_v5_accepts_amsterdam_payloads() {
2266        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
2267        let provider = Arc::new(MockEthProvider::default());
2268        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
2269        let (to_engine, mut engine_rx) = unbounded_channel();
2270
2271        let api = EngineApi::new(
2272            provider,
2273            chain_spec.clone(),
2274            ConsensusEngineHandle::new(to_engine),
2275            payload_store.into(),
2276            NoopTransactionPool::default(),
2277            Runtime::test(),
2278            ClientVersionV1 {
2279                code: ClientCode::RH,
2280                name: "Reth".to_string(),
2281                version: "v0.0.0-test".to_string(),
2282                commit: "test".to_string(),
2283            },
2284            EngineCapabilities::default(),
2285            EthereumEngineValidator::new(chain_spec),
2286            false,
2287            NoopNetwork::default(),
2288        );
2289
2290        tokio::spawn(async move {
2291            let payload_v1 = ExecutionPayloadV1::from_block_slow(&Block::default());
2292            let payload = ExecutionPayloadV4 {
2293                payload_inner: ExecutionPayloadV3 {
2294                    payload_inner: ExecutionPayloadV2 {
2295                        payload_inner: payload_v1,
2296                        withdrawals: Vec::new(),
2297                    },
2298                    blob_gas_used: 0,
2299                    excess_blob_gas: 0,
2300                },
2301                block_access_list: Bytes::from_static(b"bal"),
2302                slot_number: 1,
2303            };
2304            let execution_data = ExecutionData {
2305                payload: payload.into(),
2306                sidecar: ExecutionPayloadSidecar::v4(
2307                    CancunPayloadFields {
2308                        versioned_hashes: Vec::new(),
2309                        parent_beacon_block_root: B256::ZERO,
2310                    },
2311                    PraguePayloadFields { requests: RequestsOrHash::Requests(Requests::default()) },
2312                ),
2313            };
2314
2315            api.new_payload_v5(execution_data).await.unwrap();
2316        });
2317
2318        assert_matches!(engine_rx.recv().await, Some(BeaconEngineMessage::NewPayload { .. }));
2319    }
2320
2321    /// Returns an engine API on an Osaka-active chain where Amsterdam is not scheduled.
2322    fn setup_pre_amsterdam_engine_api() -> (
2323        EngineApiTestHandle,
2324        EngineApi<
2325            Arc<MockEthProvider>,
2326            EthEngineTypes,
2327            NoopTransactionPool,
2328            EthereumEngineValidator,
2329            ChainSpec,
2330        >,
2331    ) {
2332        let chain_spec = Arc::new(ChainSpecBuilder::mainnet().osaka_activated().build());
2333        assert!(!chain_spec.is_amsterdam_active_at_timestamp(0));
2334        let provider = Arc::new(MockEthProvider::default());
2335        let (to_engine, engine_rx) = unbounded_channel();
2336
2337        let api = EngineApi::new(
2338            provider.clone(),
2339            chain_spec.clone(),
2340            ConsensusEngineHandle::new(to_engine),
2341            spawn_test_payload_service::<EthEngineTypes>().into(),
2342            NoopTransactionPool::default(),
2343            Runtime::test(),
2344            ClientVersionV1 {
2345                code: ClientCode::RH,
2346                name: "Reth".to_string(),
2347                version: "v0.0.0-test".to_string(),
2348                commit: "test".to_string(),
2349            },
2350            EngineCapabilities::default(),
2351            EthereumEngineValidator::new(chain_spec.clone()),
2352            false,
2353            NoopNetwork::default(),
2354        );
2355        (EngineApiTestHandle { chain_spec, provider, from_api: engine_rx }, api)
2356    }
2357
2358    /// A complete `ExecutionPayloadV4` payload, including the block access list and slot number.
2359    fn complete_amsterdam_execution_data() -> ExecutionData {
2360        let payload_v1 = ExecutionPayloadV1::from_block_slow(&Block::default());
2361        let payload = ExecutionPayloadV4 {
2362            payload_inner: ExecutionPayloadV3 {
2363                payload_inner: ExecutionPayloadV2 {
2364                    payload_inner: payload_v1,
2365                    withdrawals: Vec::new(),
2366                },
2367                blob_gas_used: 0,
2368                excess_blob_gas: 0,
2369            },
2370            block_access_list: Bytes::from_static(b"bal"),
2371            slot_number: 1,
2372        };
2373        ExecutionData {
2374            payload: payload.into(),
2375            sidecar: ExecutionPayloadSidecar::v4(
2376                CancunPayloadFields {
2377                    versioned_hashes: Vec::new(),
2378                    parent_beacon_block_root: B256::ZERO,
2379                },
2380                PraguePayloadFields { requests: RequestsOrHash::Requests(Requests::default()) },
2381            ),
2382        }
2383    }
2384
2385    #[tokio::test]
2386    async fn new_payload_v5_rejects_pre_amsterdam_payloads() {
2387        let (mut handle, api) = setup_pre_amsterdam_engine_api();
2388
2389        let err = api
2390            .new_payload_v5(complete_amsterdam_execution_data())
2391            .await
2392            .expect_err("pre-Amsterdam payload must be rejected");
2393        assert_matches!(
2394            err,
2395            EngineApiError::EngineObjectValidationError(
2396                EngineObjectValidationError::UnsupportedFork
2397            )
2398        );
2399        let err = ErrorObject::from(err);
2400        assert_eq!(err.code(), UNSUPPORTED_FORK_CODE);
2401
2402        assert!(handle.from_api.try_recv().is_err(), "rejected payload must not reach the engine");
2403    }
2404
2405    #[tokio::test]
2406    async fn new_payload_v6_rejects_pre_amsterdam_payloads() {
2407        let (mut handle, api) = setup_pre_amsterdam_engine_api();
2408
2409        let err = api
2410            .new_payload_v6(complete_amsterdam_execution_data())
2411            .await
2412            .expect_err("pre-Amsterdam payload must be rejected");
2413        assert_matches!(
2414            err,
2415            EngineApiError::EngineObjectValidationError(
2416                EngineObjectValidationError::UnsupportedFork
2417            )
2418        );
2419        let err = ErrorObject::from(err);
2420        assert_eq!(err.code(), UNSUPPORTED_FORK_CODE);
2421
2422        assert!(handle.from_api.try_recv().is_err(), "rejected payload must not reach the engine");
2423    }
2424
2425    #[tokio::test]
2426    async fn fcu_v4_rejects_pre_amsterdam_payload_attributes() {
2427        let (mut handle, api) = setup_pre_amsterdam_engine_api();
2428
2429        let state = ForkchoiceState {
2430            head_block_hash: B256::repeat_byte(0x55),
2431            safe_block_hash: B256::ZERO,
2432            finalized_block_hash: B256::ZERO,
2433        };
2434        let payload_attributes = PayloadAttributes {
2435            timestamp: 1,
2436            prev_randao: B256::ZERO,
2437            suggested_fee_recipient: Address::ZERO,
2438            withdrawals: Some(vec![]),
2439            parent_beacon_block_root: Some(B256::ZERO),
2440            slot_number: Some(1),
2441            ..Default::default()
2442        };
2443
2444        let api_task = tokio::spawn(async move {
2445            api.fork_choice_updated_v4(state, Some(payload_attributes), None).await
2446        });
2447
2448        // Invalid attributes never start a build, the forkchoice update is applied without them.
2449        let request =
2450            tokio::time::timeout(std::time::Duration::from_secs(1), handle.from_api.recv())
2451                .await
2452                .expect("timed out waiting for forkchoiceUpdated request")
2453                .expect("expected forkchoiceUpdated request");
2454        let response_tx = match request {
2455            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2456                assert!(payload_attrs.is_none());
2457                tx
2458            }
2459            other => panic!("unexpected engine message: {other:?}"),
2460        };
2461        response_tx
2462            .send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
2463                PayloadStatusEnum::Valid,
2464            ))))
2465            .expect("send valid response");
2466
2467        let err = api_task
2468            .await
2469            .expect("api task should not panic")
2470            .expect_err("pre-Amsterdam attributes must be rejected");
2471        assert_matches!(
2472            err,
2473            EngineApiError::EngineObjectValidationError(
2474                EngineObjectValidationError::UnsupportedFork
2475            )
2476        );
2477        let err = ErrorObject::from(err);
2478        assert_eq!(err.code(), UNSUPPORTED_FORK_CODE);
2479    }
2480
2481    #[derive(Clone)]
2482    struct TestNetworkInfo {
2483        syncing: bool,
2484    }
2485
2486    impl NetworkInfo for TestNetworkInfo {
2487        fn local_addr(&self) -> std::net::SocketAddr {
2488            (std::net::Ipv4Addr::UNSPECIFIED, 0).into()
2489        }
2490
2491        async fn network_status(&self) -> Result<NetworkStatus, NetworkError> {
2492            #[allow(deprecated)]
2493            Ok(NetworkStatus {
2494                client_version: "test".to_string(),
2495                protocol_version: 5,
2496                eth_protocol_info: EthProtocolInfo {
2497                    network: 1,
2498                    difficulty: None,
2499                    genesis: Default::default(),
2500                    config: Default::default(),
2501                    head: Default::default(),
2502                },
2503                capabilities: vec![],
2504            })
2505        }
2506
2507        fn chain_id(&self) -> u64 {
2508            1
2509        }
2510
2511        fn cell_custody(&self) -> &CellCustody {
2512            static CELL_CUSTODY: std::sync::OnceLock<CellCustody> = std::sync::OnceLock::new();
2513            CELL_CUSTODY.get_or_init(CellCustody::default)
2514        }
2515
2516        fn is_syncing(&self) -> bool {
2517            self.syncing
2518        }
2519
2520        fn is_initially_syncing(&self) -> bool {
2521            self.syncing
2522        }
2523    }
2524
2525    #[tokio::test]
2526    async fn get_blobs_v3_returns_null_when_syncing() {
2527        let chain_spec: Arc<ChainSpec> =
2528            Arc::new(ChainSpecBuilder::mainnet().osaka_activated().build());
2529        let provider = Arc::new(MockEthProvider::default());
2530        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
2531        let (to_engine, _engine_rx) = unbounded_channel::<BeaconEngineMessage<EthEngineTypes>>();
2532
2533        let api = EngineApi::new(
2534            provider,
2535            chain_spec.clone(),
2536            ConsensusEngineHandle::new(to_engine),
2537            payload_store.into(),
2538            NoopTransactionPool::default(),
2539            Runtime::test(),
2540            ClientVersionV1 {
2541                code: ClientCode::RH,
2542                name: "Reth".to_string(),
2543                version: "v0.0.0-test".to_string(),
2544                commit: "test".to_string(),
2545            },
2546            EngineCapabilities::default(),
2547            EthereumEngineValidator::new(chain_spec),
2548            false,
2549            TestNetworkInfo { syncing: true },
2550        );
2551
2552        let res = api.get_blobs_v3_metered(vec![B256::ZERO]);
2553        assert_matches!(res, Ok(None));
2554    }
2555
2556    #[tokio::test]
2557    async fn get_blobs_v4_returns_null_when_syncing() {
2558        let chain_spec: Arc<ChainSpec> =
2559            Arc::new(ChainSpecBuilder::mainnet().osaka_activated().build());
2560        let provider = Arc::new(MockEthProvider::default());
2561        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
2562        let (to_engine, _engine_rx) = unbounded_channel::<BeaconEngineMessage<EthEngineTypes>>();
2563
2564        let api = EngineApi::new(
2565            provider,
2566            chain_spec.clone(),
2567            ConsensusEngineHandle::new(to_engine),
2568            payload_store.into(),
2569            NoopTransactionPool::default(),
2570            Runtime::test(),
2571            ClientVersionV1 {
2572                code: ClientCode::RH,
2573                name: "Reth".to_string(),
2574                version: "v0.0.0-test".to_string(),
2575                commit: "test".to_string(),
2576            },
2577            EngineCapabilities::default(),
2578            EthereumEngineValidator::new(chain_spec),
2579            false,
2580            TestNetworkInfo { syncing: true },
2581        );
2582
2583        let res = api.get_blobs_v4_metered(vec![B256::ZERO], B128::from(1u128.to_le_bytes()));
2584        assert_matches!(res, Ok(None));
2585    }
2586
2587    #[tokio::test]
2588    async fn fcu_v4_updates_shared_cell_custody_before_forkchoice_result() {
2589        let chain_spec: Arc<ChainSpec> =
2590            Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
2591        let provider = Arc::new(MockEthProvider::default());
2592        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
2593        let (to_engine, mut engine_rx) = unbounded_channel();
2594        let network = NoopNetwork::default();
2595        let cell_custody = network.cell_custody().clone();
2596
2597        let api = EngineApi::new(
2598            provider,
2599            chain_spec.clone(),
2600            ConsensusEngineHandle::new(to_engine),
2601            payload_store.into(),
2602            NoopTransactionPool::default(),
2603            Runtime::test(),
2604            ClientVersionV1 {
2605                code: ClientCode::RH,
2606                name: "Reth".to_string(),
2607                version: "v0.0.0-test".to_string(),
2608                commit: "test".to_string(),
2609            },
2610            EngineCapabilities::default(),
2611            EthereumEngineValidator::new(chain_spec),
2612            false,
2613            network,
2614        );
2615
2616        let state = ForkchoiceState {
2617            head_block_hash: B256::repeat_byte(0x33),
2618            safe_block_hash: B256::ZERO,
2619            finalized_block_hash: B256::ZERO,
2620        };
2621        let custody_columns = B128::from(0b1010u128.to_le_bytes());
2622        let expected_custody_columns = B128::from(0b1010u128);
2623
2624        let api_task = tokio::spawn(async move {
2625            api.fork_choice_updated_v4(state, None, Some(custody_columns)).await
2626        });
2627
2628        let request = tokio::time::timeout(std::time::Duration::from_secs(1), engine_rx.recv())
2629            .await
2630            .expect("timed out waiting for forkchoiceUpdated request")
2631            .expect("expected forkchoiceUpdated request");
2632        let response_tx = match request {
2633            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2634                assert!(payload_attrs.is_none());
2635                tx
2636            }
2637            other => panic!("unexpected engine message: {other:?}"),
2638        };
2639        assert_eq!(cell_custody.get(), expected_custody_columns);
2640
2641        response_tx
2642            .send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
2643                PayloadStatusEnum::Valid,
2644            ))))
2645            .expect("send valid response");
2646
2647        api_task
2648            .await
2649            .expect("api task should not panic")
2650            .expect("forkchoiceUpdatedV4 should succeed");
2651        assert_eq!(cell_custody.get(), expected_custody_columns);
2652    }
2653
2654    #[tokio::test]
2655    async fn fcu_v4_updates_shared_cell_custody_when_payload_attrs_invalid() {
2656        let chain_spec: Arc<ChainSpec> =
2657            Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build());
2658        let provider = Arc::new(MockEthProvider::default());
2659        let payload_store = spawn_test_payload_service::<EthEngineTypes>();
2660        let (to_engine, mut engine_rx) = unbounded_channel();
2661        let network = NoopNetwork::default();
2662        let cell_custody = network.cell_custody().clone();
2663
2664        let api = EngineApi::new(
2665            provider,
2666            chain_spec.clone(),
2667            ConsensusEngineHandle::new(to_engine),
2668            payload_store.into(),
2669            NoopTransactionPool::default(),
2670            Runtime::test(),
2671            ClientVersionV1 {
2672                code: ClientCode::RH,
2673                name: "Reth".to_string(),
2674                version: "v0.0.0-test".to_string(),
2675                commit: "test".to_string(),
2676            },
2677            EngineCapabilities::default(),
2678            EthereumEngineValidator::new(chain_spec),
2679            false,
2680            network,
2681        );
2682
2683        let state = ForkchoiceState {
2684            head_block_hash: B256::repeat_byte(0x44),
2685            safe_block_hash: B256::ZERO,
2686            finalized_block_hash: B256::ZERO,
2687        };
2688        let payload_attributes = PayloadAttributes {
2689            timestamp: 1,
2690            prev_randao: B256::ZERO,
2691            suggested_fee_recipient: Address::ZERO,
2692            withdrawals: Some(vec![]),
2693            parent_beacon_block_root: None,
2694            slot_number: None,
2695            ..Default::default()
2696        };
2697        let custody_columns = B128::from(0b1010u128.to_le_bytes());
2698        let expected_custody_columns = B128::from(0b1010u128);
2699
2700        let api_task = tokio::spawn(async move {
2701            api.fork_choice_updated_v4(state, Some(payload_attributes), Some(custody_columns)).await
2702        });
2703
2704        let request = tokio::time::timeout(std::time::Duration::from_secs(1), engine_rx.recv())
2705            .await
2706            .expect("timed out waiting for forkchoiceUpdated request")
2707            .expect("expected forkchoiceUpdated request");
2708        let response_tx = match request {
2709            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2710                assert!(
2711                    payload_attrs.is_none(),
2712                    "when attrs are invalid, API should first evaluate forkchoice without attrs"
2713                );
2714                tx
2715            }
2716            other => panic!("unexpected engine message: {other:?}"),
2717        };
2718        assert_eq!(cell_custody.get(), expected_custody_columns);
2719
2720        response_tx
2721            .send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
2722                PayloadStatusEnum::Valid,
2723            ))))
2724            .expect("send valid response");
2725
2726        let response = api_task.await.expect("api task should not panic");
2727        assert_matches!(
2728            response,
2729            Err(EngineApiError::EngineObjectValidationError(
2730                reth_payload_primitives::EngineObjectValidationError::PayloadAttributes(_)
2731            ))
2732        );
2733    }
2734
2735    #[tokio::test]
2736    async fn fcu_v3_syncing_precedes_invalid_payload_attributes_validation() {
2737        let (mut handle, api) = setup_engine_api();
2738
2739        let state = ForkchoiceState {
2740            head_block_hash: B256::repeat_byte(0x11),
2741            safe_block_hash: B256::ZERO,
2742            finalized_block_hash: B256::ZERO,
2743        };
2744        let payload_attributes = PayloadAttributes {
2745            timestamp: 1,
2746            prev_randao: B256::ZERO,
2747            suggested_fee_recipient: Address::ZERO,
2748            withdrawals: Some(vec![]),
2749            // Invalid for V3/Cancun, but should be ignored if forkchoice is SYNCING.
2750            parent_beacon_block_root: None,
2751            slot_number: None,
2752            ..Default::default()
2753        };
2754
2755        let api_task = tokio::spawn(async move {
2756            api.fork_choice_updated_v3(state, Some(payload_attributes)).await
2757        });
2758
2759        let request =
2760            tokio::time::timeout(std::time::Duration::from_secs(1), handle.from_api.recv())
2761                .await
2762                .expect("timed out waiting for forkchoiceUpdated request")
2763                .expect("expected forkchoiceUpdated request");
2764        let response_tx = match request {
2765            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2766                assert!(
2767                    payload_attrs.is_none(),
2768                    "FCU for syncing state should be evaluated before payload attributes"
2769                );
2770                tx
2771            }
2772            other => panic!("unexpected engine message: {other:?}"),
2773        };
2774
2775        response_tx.send(Ok(OnForkChoiceUpdated::syncing())).expect("send syncing response");
2776
2777        let response = api_task
2778            .await
2779            .expect("api task should not panic")
2780            .expect("forkchoiceUpdatedV3 should return a syncing response");
2781        assert!(response.payload_status.is_syncing());
2782        assert!(response.payload_id.is_none());
2783    }
2784
2785    #[tokio::test]
2786    async fn fcu_v3_valid_forkchoice_missing_beacon_root_returns_invalid_attributes() {
2787        let (mut handle, api) = setup_engine_api();
2788
2789        let state = ForkchoiceState {
2790            head_block_hash: B256::repeat_byte(0x22),
2791            safe_block_hash: B256::ZERO,
2792            finalized_block_hash: B256::ZERO,
2793        };
2794        let payload_attributes = PayloadAttributes {
2795            timestamp: 1,
2796            prev_randao: B256::ZERO,
2797            suggested_fee_recipient: Address::ZERO,
2798            withdrawals: Some(vec![]),
2799            parent_beacon_block_root: None,
2800            slot_number: None,
2801            ..Default::default()
2802        };
2803
2804        let api_task = tokio::spawn(async move {
2805            api.fork_choice_updated_v3(state, Some(payload_attributes)).await
2806        });
2807
2808        let request =
2809            tokio::time::timeout(std::time::Duration::from_secs(1), handle.from_api.recv())
2810                .await
2811                .expect("timed out waiting for forkchoiceUpdated request")
2812                .expect("expected forkchoiceUpdated request");
2813
2814        let response_tx = match request {
2815            BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
2816                assert!(
2817                    payload_attrs.is_none(),
2818                    "when attrs are invalid, API should first evaluate forkchoice without attrs"
2819                );
2820                tx
2821            }
2822            other => panic!("unexpected engine message: {other:?}"),
2823        };
2824
2825        response_tx
2826            .send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
2827                PayloadStatusEnum::Valid,
2828            ))))
2829            .expect("send valid response");
2830
2831        let response = api_task.await.expect("api task should not panic");
2832        assert_matches!(
2833            response,
2834            Err(EngineApiError::EngineObjectValidationError(
2835                reth_payload_primitives::EngineObjectValidationError::PayloadAttributes(_)
2836            ))
2837        );
2838
2839        match tokio::time::timeout(std::time::Duration::from_millis(100), handle.from_api.recv())
2840            .await
2841        {
2842            Err(_) | Ok(None) => {}
2843            Ok(Some(BeaconEngineMessage::ForkchoiceUpdated { .. })) => {
2844                panic!("no second forkchoiceUpdated call should be sent when attrs are invalid")
2845            }
2846            Ok(Some(other)) => panic!("unexpected engine message: {other:?}"),
2847        }
2848    }
2849
2850    // tests covering `engine_getPayloadBodiesByRange` and `engine_getPayloadBodiesByHash`
2851    mod get_payload_bodies {
2852        use super::*;
2853        use alloy_rpc_types_engine::ExecutionPayloadBodyV1;
2854        use reth_testing_utils::generators::{self, random_block_range, BlockRangeParams};
2855
2856        #[tokio::test]
2857        async fn invalid_params() {
2858            let (_, api) = setup_engine_api();
2859
2860            let by_range_tests = [
2861                // (start, count)
2862                (0, 0),
2863                (0, 1),
2864                (1, 0),
2865            ];
2866
2867            // test [EngineApiMessage::GetPayloadBodiesByRange]
2868            for (start, count) in by_range_tests {
2869                let res = api.get_payload_bodies_by_range_v1(start, count).await;
2870                assert_matches!(res, Err(EngineApiError::InvalidBodiesRange { .. }));
2871            }
2872        }
2873
2874        #[tokio::test]
2875        async fn request_too_large() {
2876            let (_, api) = setup_engine_api();
2877
2878            let request_count = MAX_PAYLOAD_BODIES_LIMIT + 1;
2879            let res = api.get_payload_bodies_by_range_v1(0, request_count).await;
2880            assert_matches!(res, Err(EngineApiError::PayloadRequestTooLarge { .. }));
2881        }
2882
2883        #[tokio::test]
2884        async fn returns_payload_bodies() {
2885            let mut rng = generators::rng();
2886            let (handle, api) = setup_engine_api();
2887
2888            let (start, count) = (1, 10);
2889            let blocks = random_block_range(
2890                &mut rng,
2891                start..=start + count - 1,
2892                BlockRangeParams { tx_count: 0..2, ..Default::default() },
2893            );
2894            handle
2895                .provider
2896                .extend_blocks(blocks.iter().cloned().map(|b| (b.hash(), b.into_block())));
2897
2898            let expected = blocks
2899                .iter()
2900                .cloned()
2901                .map(|b| Some(ExecutionPayloadBodyV1::from_block(b.into_block())))
2902                .collect::<Vec<_>>();
2903
2904            let res = api.get_payload_bodies_by_range_v1(start, count).await.unwrap();
2905            assert_eq!(res, expected);
2906        }
2907
2908        #[tokio::test]
2909        async fn returns_payload_bodies_with_gaps() {
2910            let mut rng = generators::rng();
2911            let (handle, api) = setup_engine_api();
2912
2913            let (start, count) = (1, 100);
2914            let blocks = random_block_range(
2915                &mut rng,
2916                start..=start + count - 1,
2917                BlockRangeParams { tx_count: 0..2, ..Default::default() },
2918            );
2919
2920            // Insert only blocks in ranges 1-25 and 50-75
2921            let first_missing_range = 26..=50;
2922            let second_missing_range = 76..=100;
2923            handle.provider.extend_blocks(
2924                blocks
2925                    .iter()
2926                    .filter(|b| {
2927                        !first_missing_range.contains(&b.number) &&
2928                            !second_missing_range.contains(&b.number)
2929                    })
2930                    .map(|b| (b.hash(), b.clone().into_block())),
2931            );
2932
2933            let expected = blocks
2934                .iter()
2935                // filter anything after the second missing range to ensure we don't expect trailing
2936                // `None`s
2937                .filter(|b| !second_missing_range.contains(&b.number))
2938                .cloned()
2939                .map(|b| {
2940                    if first_missing_range.contains(&b.number) {
2941                        None
2942                    } else {
2943                        Some(ExecutionPayloadBodyV1::from_block(b.into_block()))
2944                    }
2945                })
2946                .collect::<Vec<_>>();
2947
2948            let res = api.get_payload_bodies_by_range_v1(start, count).await.unwrap();
2949            assert_eq!(res, expected);
2950
2951            let expected = blocks
2952                .iter()
2953                .cloned()
2954                // ensure we still return trailing `None`s here because by-hash will not be aware
2955                // of the missing block's number, and cannot compare it to the current best block
2956                .map(|b| {
2957                    if first_missing_range.contains(&b.number) ||
2958                        second_missing_range.contains(&b.number)
2959                    {
2960                        None
2961                    } else {
2962                        Some(ExecutionPayloadBodyV1::from_block(b.into_block()))
2963                    }
2964                })
2965                .collect::<Vec<_>>();
2966
2967            let hashes = blocks.iter().map(|b| b.hash()).collect();
2968            let res = api.get_payload_bodies_by_hash_v1(hashes).await.unwrap();
2969            assert_eq!(res, expected);
2970        }
2971    }
2972
2973    #[tokio::test]
2974    async fn new_payload_rejects_pre_shanghai_timestamps() {
2975        let (mut handle, api) = setup_engine_api();
2976        assert!(!handle.chain_spec.is_shanghai_active_at_timestamp(0));
2977
2978        let errors = tokio::time::timeout(Duration::from_secs(1), async {
2979            [
2980                api.new_payload_v5(complete_amsterdam_execution_data()).await.unwrap_err(),
2981                api.new_payload_v6(complete_amsterdam_execution_data()).await.unwrap_err(),
2982            ]
2983        })
2984        .await
2985        .expect("payload validation must complete without reaching the engine");
2986
2987        for error in errors {
2988            assert_matches!(
2989                error,
2990                EngineApiError::EngineObjectValidationError(
2991                    EngineObjectValidationError::UnsupportedFork
2992                )
2993            );
2994            assert_eq!(ErrorObject::from(error).code(), UNSUPPORTED_FORK_CODE);
2995        }
2996        assert_matches!(handle.from_api.try_recv(), Err(TryRecvError::Empty));
2997    }
2998
2999    #[tokio::test]
3000    async fn fcu_rejects_pre_shanghai_timestamps() {
3001        for version in [EngineApiMessageVersion::V4, EngineApiMessageVersion::V5] {
3002            let (mut handle, api) = setup_engine_api();
3003            assert!(!handle.chain_spec.is_shanghai_active_at_timestamp(1));
3004            let state = ForkchoiceState::same_hash(B256::repeat_byte(0x55));
3005            let attributes = PayloadAttributes {
3006                timestamp: 1,
3007                withdrawals: Some(vec![]),
3008                parent_beacon_block_root: Some(B256::ZERO),
3009                slot_number: Some(1),
3010                target_gas_limit: Some(30_000_000),
3011                ..Default::default()
3012            };
3013            let api_task = tokio::spawn(async move {
3014                match version {
3015                    EngineApiMessageVersion::V4 => {
3016                        api.fork_choice_updated_v4(state, Some(attributes), None).await.map(|_| ())
3017                    }
3018                    EngineApiMessageVersion::V5 => {
3019                        api.fork_choice_updated_v5(state, Some(attributes), None).await.map(|_| ())
3020                    }
3021                    _ => unreachable!(),
3022                }
3023            });
3024
3025            let request = tokio::time::timeout(Duration::from_secs(1), handle.from_api.recv())
3026                .await
3027                .expect("timed out waiting for forkchoiceUpdated request")
3028                .expect("expected forkchoiceUpdated request");
3029            let BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } = request else {
3030                panic!("unexpected engine message: {request:?}");
3031            };
3032            assert!(payload_attrs.is_none());
3033            tx.send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
3034                PayloadStatusEnum::Valid,
3035            ))))
3036            .expect("send valid response");
3037
3038            let error = tokio::time::timeout(Duration::from_secs(1), api_task)
3039                .await
3040                .expect("forkchoiceUpdated must complete after the engine response")
3041                .expect("api task should not panic")
3042                .unwrap_err();
3043            assert_matches!(
3044                error,
3045                EngineApiError::EngineObjectValidationError(
3046                    EngineObjectValidationError::UnsupportedFork
3047                )
3048            );
3049            assert_eq!(ErrorObject::from(error).code(), UNSUPPORTED_FORK_CODE);
3050        }
3051    }
3052}