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. Execution-cache allocations removed by those updates may still
104/// be destroyed concurrently after unlocking; their cleanup is excluded from cache wait timings.
105/// Each wait can be independently disabled via `wait_for_persistence` and `wait_for_caches`.
106///
107/// Responses include timing breakdowns with server-measured execution latency.
108#[cfg_attr(not(feature = "client"), rpc(server, namespace = "reth"))]
109#[cfg_attr(feature = "client", rpc(server, client, namespace = "reth"))]
110pub trait RethEngineApi<ExecutionData> {
111    /// Reth-specific newPayload that accepts either `ExecutionData` directly or an RLP-encoded
112    /// block.
113    ///
114    /// `wait_for_persistence` (default `true`): waits for in-flight persistence to complete.
115    /// `wait_for_caches` (default `true`): waits for execution cache and sparse trie locks.
116    #[method(name = "newPayload")]
117    async fn reth_new_payload(
118        &self,
119        payload: RethNewPayloadInput<ExecutionData>,
120        wait_for_persistence: Option<bool>,
121        wait_for_caches: Option<bool>,
122    ) -> RpcResult<RethPayloadStatus>;
123
124    /// Reth-specific forkchoiceUpdated that sends a regular forkchoice update with no payload
125    /// attributes.
126    #[method(name = "forkchoiceUpdated")]
127    async fn reth_forkchoice_updated(
128        &self,
129        forkchoice_state: ForkchoiceState,
130    ) -> RpcResult<ForkchoiceUpdated>;
131}
132
133#[cfg(test)]
134mod tests {
135    use super::RethNewPayloadInput;
136    use alloy_primitives::Bytes;
137    use serde::{Deserialize, Serialize};
138    use serde_json::json;
139
140    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141    struct TestExecutionData {
142        payload: Bytes,
143        sidecar: Bytes,
144    }
145
146    #[test]
147    fn block_rlp_serializes_named_fields() {
148        let input = RethNewPayloadInput::<TestExecutionData>::BlockRlp {
149            block: Bytes::from_static(&[1]),
150            bal: Some(Bytes::from_static(&[2])),
151        };
152
153        assert_eq!(serde_json::to_string(&input).unwrap(), r#"{"block":"0x01","bal":"0x02"}"#);
154        assert_eq!(serde_json::to_value(input).unwrap(), json!({ "block": "0x01", "bal": "0x02" }));
155    }
156
157    #[test]
158    fn block_rlp_without_bal_serializes_legacy_bytes_roundtrip() {
159        let input = RethNewPayloadInput::<TestExecutionData>::BlockRlp {
160            block: Bytes::from_static(&[1]),
161            bal: None,
162        };
163
164        let serialized = serde_json::to_string(&input).unwrap();
165        assert_eq!(serialized, r#""0x01""#);
166
167        let input =
168            serde_json::from_str::<RethNewPayloadInput<TestExecutionData>>(&serialized).unwrap();
169
170        let RethNewPayloadInput::BlockRlp { block, bal } = input else {
171            panic!("expected block rlp input")
172        };
173
174        assert_eq!(block, Bytes::from_static(&[1]));
175        assert_eq!(bal, None);
176    }
177
178    #[test]
179    fn block_rlp_deserializes_without_bal() {
180        let input =
181            serde_json::from_str::<RethNewPayloadInput<TestExecutionData>>(r#"{"block":"0x01"}"#)
182                .unwrap();
183
184        let RethNewPayloadInput::BlockRlp { block, bal } = input else {
185            panic!("expected block rlp input")
186        };
187
188        assert_eq!(block, Bytes::from_static(&[1]));
189        assert_eq!(bal, None);
190    }
191
192    #[test]
193    fn block_rlp_deserializes_legacy_bytes() {
194        let input = serde_json::from_value::<RethNewPayloadInput<TestExecutionData>>(json!("0x01"))
195            .unwrap();
196
197        let RethNewPayloadInput::BlockRlp { block, bal } = input else {
198            panic!("expected block rlp input")
199        };
200
201        assert_eq!(block, Bytes::from_static(&[1]));
202        assert_eq!(bal, None);
203    }
204}