reth_e2e_test_utils/engine.rs
1//! Version-agnostic access to the consensus engine of a test node.
2
3use alloy_primitives::Bytes;
4use alloy_rpc_types_engine::{ForkchoiceState, ForkchoiceUpdated, PayloadStatus};
5use reth_engine_primitives::{
6 BeaconForkChoiceUpdateError, BeaconOnNewPayloadError, ConsensusEngineHandle,
7};
8use reth_payload_primitives::{BuiltPayload, PayloadTypes};
9use reth_primitives_traits::{NodePrimitives, SealedBlock};
10
11/// Sends `newPayload` and `forkchoiceUpdated` messages to the consensus engine of a test node like
12/// a consensus client, without picking an Engine API version for the active fork.
13///
14/// The messages go to the engine in-process and skip the Engine API RPC layer. That layer decodes
15/// the JSON or SSZ request, rejects a method version that does not match the fork of the payload
16/// or payload attributes timestamp, checks that the fields of that fork are present, and maps the
17/// errors of the engine to RPC error codes. None of this happens here, so tests of that behaviour
18/// must call the versioned RPC methods, e.g. with [`EngineApiClient`] on the client of
19/// [`NodeTestContext::auth_server_handle`].
20///
21/// [`EngineApiClient`]: reth_rpc_api::EngineApiClient
22/// [`NodeTestContext::auth_server_handle`]: crate::node::NodeTestContext::auth_server_handle
23#[derive(Debug, Clone)]
24pub struct EngineTestContext<T: PayloadTypes> {
25 handle: ConsensusEngineHandle<T>,
26}
27
28impl<T: PayloadTypes> EngineTestContext<T> {
29 /// Creates a new engine context that sends its messages to the given engine handle.
30 pub const fn new(handle: ConsensusEngineHandle<T>) -> Self {
31 Self { handle }
32 }
33
34 /// Sends a payload to the engine like `engine_newPayload` and returns the payload status.
35 ///
36 /// The engine inserts the block without making it canonical, see [`Self::forkchoice_updated`].
37 /// An invalid block is reported as an `INVALID` status, not as an error. A built payload of the
38 /// node converts into the payload type of the engine.
39 ///
40 /// Returns an error if the engine can not process the payload at all, e.g. because its
41 /// parameters are malformed, which the Engine API returns as an invalid params error.
42 pub async fn new_payload(
43 &self,
44 payload: impl Into<T::ExecutionData>,
45 ) -> Result<PayloadStatus, BeaconOnNewPayloadError> {
46 self.handle.new_payload(payload.into()).await
47 }
48
49 /// Sends the payload of a sealed block to the engine, see [`Self::new_payload`].
50 ///
51 /// `block_access_list` is the encoded block access list of the block, which payloads carry
52 /// from Amsterdam on.
53 pub async fn new_payload_from_block(
54 &self,
55 block: SealedBlock<
56 <<T::BuiltPayload as BuiltPayload>::Primitives as NodePrimitives>::Block,
57 >,
58 block_access_list: Option<Bytes>,
59 ) -> Result<PayloadStatus, BeaconOnNewPayloadError> {
60 self.new_payload(T::block_to_payload(block, block_access_list)).await
61 }
62
63 /// Sends a forkchoice update without payload attributes to the engine like
64 /// `engine_forkchoiceUpdated` and returns the response.
65 ///
66 /// The payload status of the response is the outcome for the head block: `VALID` once it is
67 /// canonical, `SYNCING` if the engine does not know it or is backfilling, and `INVALID` if it
68 /// is or descends from an invalid block.
69 ///
70 /// Returns an error if the engine rejects the forkchoice state, e.g.
71 /// [`ForkchoiceUpdateError::InvalidState`] for a zero head hash, or a safe or finalized block
72 /// that is unknown or not on the chain of the head. The Engine API returns that error as code
73 /// -38002.
74 ///
75 /// [`ForkchoiceUpdateError::InvalidState`]: alloy_rpc_types_engine::ForkchoiceUpdateError::InvalidState
76 pub async fn forkchoice_updated(
77 &self,
78 state: ForkchoiceState,
79 ) -> Result<ForkchoiceUpdated, BeaconForkChoiceUpdateError> {
80 self.handle.fork_choice_updated(state, None).await
81 }
82
83 /// Sends a forkchoice update with payload attributes to the engine like
84 /// `engine_forkchoiceUpdated`, which starts a payload job on top of the head, and returns the
85 /// response with the id of the job.
86 ///
87 /// The forkchoice state is handled like in [`Self::forkchoice_updated`]. The response has no
88 /// payload id if the engine starts no payload job, e.g. because it is syncing.
89 ///
90 /// Returns [`ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes`] if the attributes are
91 /// invalid for the head, e.g. because their timestamp is not greater than the timestamp of the
92 /// head. The engine still applies the forkchoice state in that case. The Engine API returns
93 /// that error as code -38003.
94 ///
95 /// [`ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes`]: alloy_rpc_types_engine::ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes
96 pub async fn forkchoice_updated_with_attributes(
97 &self,
98 state: ForkchoiceState,
99 attributes: impl Into<T::PayloadAttributes>,
100 ) -> Result<ForkchoiceUpdated, BeaconForkChoiceUpdateError> {
101 self.handle.fork_choice_updated(state, Some(attributes.into())).await
102 }
103}