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::{Deserialize, Deserializer, Serialize};
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, Serialize)]
35#[serde(untagged)]
36pub enum RethNewPayloadInput<ExecutionData> {
37    /// Standard execution data (payload + sidecar).
38    ExecutionData(ExecutionData),
39    /// An RLP-encoded block and optional encoded block access list.
40    BlockRlp {
41        /// RLP-encoded block bytes.
42        block: Bytes,
43        /// RLP-encoded block access list bytes.
44        #[serde(default, skip_serializing_if = "Option::is_none")]
45        bal: Option<Bytes>,
46    },
47}
48
49impl<'de, ExecutionData> Deserialize<'de> for RethNewPayloadInput<ExecutionData>
50where
51    ExecutionData: Deserialize<'de>,
52{
53    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54    where
55        D: Deserializer<'de>,
56    {
57        #[derive(Deserialize)]
58        #[serde(untagged)]
59        enum RethNewPayloadInputSerde<ExecutionData> {
60            ExecutionData(ExecutionData),
61            BlockRlp {
62                block: Bytes,
63                #[serde(default)]
64                bal: Option<Bytes>,
65            },
66            LegacyBlockRlp(Bytes),
67        }
68
69        Ok(match RethNewPayloadInputSerde::deserialize(deserializer)? {
70            RethNewPayloadInputSerde::ExecutionData(data) => Self::ExecutionData(data),
71            RethNewPayloadInputSerde::BlockRlp { block, bal } => Self::BlockRlp { block, bal },
72            RethNewPayloadInputSerde::LegacyBlockRlp(block) => Self::BlockRlp { block, bal: None },
73        })
74    }
75}
76
77/// Reth-specific engine API extensions.
78///
79/// This trait provides a `reth_newPayload` endpoint that accepts either `ExecutionData` directly
80/// (payload + sidecar) or an RLP-encoded block, optionally alongside a block access list and
81/// waiting for persistence and cache locks before processing.
82///
83/// By default, the endpoint waits for both in-flight persistence and cache updates to complete
84/// before executing the payload, providing unbiased timing measurements. Each can be independently
85/// disabled via `wait_for_persistence` and `wait_for_caches`.
86///
87/// Responses include timing breakdowns with server-measured execution latency.
88#[cfg_attr(not(feature = "client"), rpc(server, namespace = "reth"))]
89#[cfg_attr(feature = "client", rpc(server, client, namespace = "reth"))]
90pub trait RethEngineApi<ExecutionData> {
91    /// Reth-specific newPayload that accepts either `ExecutionData` directly or an RLP-encoded
92    /// block.
93    ///
94    /// `wait_for_persistence` (default `true`): waits for in-flight persistence to complete.
95    /// `wait_for_caches` (default `true`): waits for execution cache and sparse trie locks.
96    #[method(name = "newPayload")]
97    async fn reth_new_payload(
98        &self,
99        payload: RethNewPayloadInput<ExecutionData>,
100        wait_for_persistence: Option<bool>,
101        wait_for_caches: Option<bool>,
102    ) -> RpcResult<RethPayloadStatus>;
103
104    /// Reth-specific forkchoiceUpdated that sends a regular forkchoice update with no payload
105    /// attributes.
106    #[method(name = "forkchoiceUpdated")]
107    async fn reth_forkchoice_updated(
108        &self,
109        forkchoice_state: ForkchoiceState,
110    ) -> RpcResult<ForkchoiceUpdated>;
111}
112
113#[cfg(test)]
114mod tests {
115    use super::RethNewPayloadInput;
116    use alloy_primitives::Bytes;
117    use serde::{Deserialize, Serialize};
118    use serde_json::json;
119
120    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121    struct TestExecutionData {
122        payload: Bytes,
123        sidecar: Bytes,
124    }
125
126    #[test]
127    fn block_rlp_serializes_named_fields() {
128        let input = RethNewPayloadInput::<TestExecutionData>::BlockRlp {
129            block: Bytes::from_static(&[1]),
130            bal: Some(Bytes::from_static(&[2])),
131        };
132
133        assert_eq!(serde_json::to_value(input).unwrap(), json!({ "block": "0x01", "bal": "0x02" }));
134    }
135
136    #[test]
137    fn block_rlp_deserializes_without_bal() {
138        let input = serde_json::from_value::<RethNewPayloadInput<TestExecutionData>>(json!({
139            "block": "0x01"
140        }))
141        .unwrap();
142
143        let RethNewPayloadInput::BlockRlp { block, bal } = input else {
144            panic!("expected block rlp input")
145        };
146
147        assert_eq!(block, Bytes::from_static(&[1]));
148        assert_eq!(bal, None);
149    }
150
151    #[test]
152    fn block_rlp_deserializes_legacy_bytes() {
153        let input = serde_json::from_value::<RethNewPayloadInput<TestExecutionData>>(json!("0x01"))
154            .unwrap();
155
156        let RethNewPayloadInput::BlockRlp { block, bal } = input else {
157            panic!("expected block rlp input")
158        };
159
160        assert_eq!(block, Bytes::from_static(&[1]));
161        assert_eq!(bal, None);
162    }
163}