Skip to main content

reth_rpc_api/
reth_engine.rs

1//! Reth-specific engine API extensions.
2
3use alloy_primitives::Bytes;
4use alloy_rpc_types_engine::{ForkchoiceState, ForkchoiceUpdated, PayloadStatus};
5use jsonrpsee::{core::RpcResult, proc_macros::rpc};
6use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
7
8/// Reth-specific payload status that includes server-measured execution latency.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RethPayloadStatus {
11    /// The standard payload status.
12    #[serde(flatten)]
13    pub status: PayloadStatus,
14    /// Server-side execution latency in microseconds.
15    pub latency_us: u64,
16    /// Time spent waiting on persistence in microseconds, including both time spent
17    /// queued due to persistence backpressure and, when requested, the explicit wait
18    /// for in-flight persistence to complete.
19    pub persistence_wait_us: u64,
20    /// Time spent waiting for the execution cache lock, in microseconds.
21    ///
22    /// `None` when wasn't asked to wait.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub execution_cache_wait_us: Option<u64>,
25    /// Time spent waiting for the sparse trie lock, in microseconds.
26    ///
27    /// `None` when wasn't asked to wait.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub sparse_trie_wait_us: Option<u64>,
30}
31
32/// Input for `reth_newPayload` that accepts either `ExecutionData` directly or an RLP-encoded
33/// block with optional side data.
34#[derive(Debug, Clone)]
35pub enum RethNewPayloadInput<ExecutionData> {
36    /// Standard execution data (payload + sidecar).
37    ExecutionData(ExecutionData),
38    /// An RLP-encoded block and optional encoded block access list.
39    BlockRlp {
40        /// RLP-encoded block bytes.
41        block: Bytes,
42        /// RLP-encoded block access list bytes.
43        bal: Option<Bytes>,
44    },
45}
46
47impl<ExecutionData> Serialize for RethNewPayloadInput<ExecutionData>
48where
49    ExecutionData: Serialize,
50{
51    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
52    where
53        S: Serializer,
54    {
55        match self {
56            Self::ExecutionData(data) => data.serialize(serializer),
57            Self::BlockRlp { block, bal: None } => block.serialize(serializer),
58            Self::BlockRlp { block, bal: Some(bal) } => {
59                let mut state = serializer.serialize_struct("BlockRlp", 2)?;
60                state.serialize_field("block", block)?;
61                state.serialize_field("bal", bal)?;
62                state.end()
63            }
64        }
65    }
66}
67
68impl<'de, ExecutionData> Deserialize<'de> for RethNewPayloadInput<ExecutionData>
69where
70    ExecutionData: Deserialize<'de>,
71{
72    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73    where
74        D: Deserializer<'de>,
75    {
76        #[derive(Deserialize)]
77        #[serde(untagged)]
78        enum RethNewPayloadInputSerde<ExecutionData> {
79            ExecutionData(ExecutionData),
80            BlockRlp {
81                block: Bytes,
82                #[serde(default)]
83                bal: Option<Bytes>,
84            },
85            LegacyBlockRlp(Bytes),
86        }
87
88        Ok(match RethNewPayloadInputSerde::deserialize(deserializer)? {
89            RethNewPayloadInputSerde::ExecutionData(data) => Self::ExecutionData(data),
90            RethNewPayloadInputSerde::BlockRlp { block, bal } => Self::BlockRlp { block, bal },
91            RethNewPayloadInputSerde::LegacyBlockRlp(block) => Self::BlockRlp { block, bal: None },
92        })
93    }
94}
95
96/// Reth-specific engine API extensions.
97///
98/// This trait provides a `reth_newPayload` endpoint that accepts either `ExecutionData` directly
99/// (payload + sidecar) or an RLP-encoded block, optionally alongside a block access list and
100/// waiting for persistence and cache locks before processing.
101///
102/// By default, the endpoint waits for both in-flight persistence and cache updates to complete
103/// before executing the payload, providing unbiased timing measurements. Each can be independently
104/// disabled via `wait_for_persistence` and `wait_for_caches`.
105///
106/// Responses include timing breakdowns with server-measured execution latency.
107#[cfg_attr(not(feature = "client"), rpc(server, namespace = "reth"))]
108#[cfg_attr(feature = "client", rpc(server, client, namespace = "reth"))]
109pub trait RethEngineApi<ExecutionData> {
110    /// Reth-specific newPayload that accepts either `ExecutionData` directly or an RLP-encoded
111    /// block.
112    ///
113    /// `wait_for_persistence` (default `true`): waits for in-flight persistence to complete.
114    /// `wait_for_caches` (default `true`): waits for execution cache and sparse trie locks.
115    #[method(name = "newPayload")]
116    async fn reth_new_payload(
117        &self,
118        payload: RethNewPayloadInput<ExecutionData>,
119        wait_for_persistence: Option<bool>,
120        wait_for_caches: Option<bool>,
121    ) -> RpcResult<RethPayloadStatus>;
122
123    /// Reth-specific forkchoiceUpdated that sends a regular forkchoice update with no payload
124    /// attributes.
125    #[method(name = "forkchoiceUpdated")]
126    async fn reth_forkchoice_updated(
127        &self,
128        forkchoice_state: ForkchoiceState,
129    ) -> RpcResult<ForkchoiceUpdated>;
130}
131
132#[cfg(test)]
133mod tests {
134    use super::RethNewPayloadInput;
135    use alloy_primitives::Bytes;
136    use serde::{Deserialize, Serialize};
137    use serde_json::json;
138
139    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140    struct TestExecutionData {
141        payload: Bytes,
142        sidecar: Bytes,
143    }
144
145    #[test]
146    fn block_rlp_serializes_named_fields() {
147        let input = RethNewPayloadInput::<TestExecutionData>::BlockRlp {
148            block: Bytes::from_static(&[1]),
149            bal: Some(Bytes::from_static(&[2])),
150        };
151
152        assert_eq!(serde_json::to_string(&input).unwrap(), r#"{"block":"0x01","bal":"0x02"}"#);
153        assert_eq!(serde_json::to_value(input).unwrap(), json!({ "block": "0x01", "bal": "0x02" }));
154    }
155
156    #[test]
157    fn block_rlp_without_bal_serializes_legacy_bytes_roundtrip() {
158        let input = RethNewPayloadInput::<TestExecutionData>::BlockRlp {
159            block: Bytes::from_static(&[1]),
160            bal: None,
161        };
162
163        let serialized = serde_json::to_string(&input).unwrap();
164        assert_eq!(serialized, r#""0x01""#);
165
166        let input =
167            serde_json::from_str::<RethNewPayloadInput<TestExecutionData>>(&serialized).unwrap();
168
169        let RethNewPayloadInput::BlockRlp { block, bal } = input else {
170            panic!("expected block rlp input")
171        };
172
173        assert_eq!(block, Bytes::from_static(&[1]));
174        assert_eq!(bal, None);
175    }
176
177    #[test]
178    fn block_rlp_deserializes_without_bal() {
179        let input =
180            serde_json::from_str::<RethNewPayloadInput<TestExecutionData>>(r#"{"block":"0x01"}"#)
181                .unwrap();
182
183        let RethNewPayloadInput::BlockRlp { block, bal } = input else {
184            panic!("expected block rlp input")
185        };
186
187        assert_eq!(block, Bytes::from_static(&[1]));
188        assert_eq!(bal, None);
189    }
190
191    #[test]
192    fn block_rlp_deserializes_legacy_bytes() {
193        let input = serde_json::from_value::<RethNewPayloadInput<TestExecutionData>>(json!("0x01"))
194            .unwrap();
195
196        let RethNewPayloadInput::BlockRlp { block, bal } = input else {
197            panic!("expected block rlp input")
198        };
199
200        assert_eq!(block, Bytes::from_static(&[1]));
201        assert_eq!(bal, None);
202    }
203}