Skip to main content

reth_rpc_engine_api/
capabilities.rs

1//! Engine API capabilities.
2
3use std::collections::HashSet;
4use tracing::warn;
5
6/// Critical Engine API method prefixes that warrant warnings on capability mismatches.
7///
8/// These are essential for block production and chain synchronization. Missing support
9/// for these methods indicates a significant version mismatch that operators should address.
10const CRITICAL_METHOD_PREFIXES: &[&str] =
11    &["engine_forkchoiceUpdated", "engine_getPayload", "engine_newPayload"];
12
13/// Engine API methods for upcoming hardforks that should not warn while they are still unstable.
14const UNSTABLE_METHODS: &[&str] =
15    &["engine_forkchoiceUpdatedV4", "engine_getPayloadV6", "engine_newPayloadV5"];
16
17/// All Engine API capabilities supported by Reth (Ethereum mainnet).
18///
19/// See <https://github.com/ethereum/execution-apis/tree/main/src/engine> for updates.
20pub const CAPABILITIES: &[&str] = &[
21    "engine_forkchoiceUpdatedV1",
22    "engine_forkchoiceUpdatedV2",
23    "engine_forkchoiceUpdatedV3",
24    "engine_forkchoiceUpdatedV4",
25    "engine_getClientVersionV1",
26    "engine_getPayloadV1",
27    "engine_getPayloadV2",
28    "engine_getPayloadV3",
29    "engine_getPayloadV4",
30    "engine_getPayloadV5",
31    "engine_getPayloadV6",
32    "engine_newPayloadV1",
33    "engine_newPayloadV2",
34    "engine_newPayloadV3",
35    "engine_newPayloadV4",
36    "engine_newPayloadV5",
37    "engine_getPayloadBodiesByHashV1",
38    "engine_getPayloadBodiesByHashV2",
39    "engine_getPayloadBodiesByRangeV1",
40    "engine_getPayloadBodiesByRangeV2",
41    "engine_getBlobsV1",
42    "engine_getBlobsV2",
43    "engine_getBlobsV3",
44    "engine_getBlobsV4",
45];
46
47/// Engine API capabilities set.
48#[derive(Debug, Clone)]
49pub struct EngineCapabilities {
50    inner: HashSet<String>,
51}
52
53impl EngineCapabilities {
54    /// Creates from an iterator of capability strings.
55    pub fn new(capabilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
56        Self { inner: capabilities.into_iter().map(Into::into).collect() }
57    }
58
59    /// Returns the capabilities as a list of strings.
60    pub fn list(&self) -> Vec<String> {
61        self.inner.iter().cloned().collect()
62    }
63
64    /// Returns a reference to the inner set.
65    pub const fn as_set(&self) -> &HashSet<String> {
66        &self.inner
67    }
68
69    /// Compares CL capabilities with this EL's capabilities and returns any mismatches.
70    ///
71    /// Called during `engine_exchangeCapabilities` to detect version mismatches
72    /// between the consensus layer and execution layer.
73    pub fn get_capability_mismatches(&self, cl_capabilities: &[String]) -> CapabilityMismatches {
74        let cl_set: HashSet<&str> = cl_capabilities.iter().map(String::as_str).collect();
75
76        // CL has methods EL doesn't support
77        let mut missing_in_el: Vec<_> = cl_capabilities
78            .iter()
79            .filter(|cap| !self.inner.contains(cap.as_str()))
80            .cloned()
81            .collect();
82        missing_in_el.sort_unstable();
83
84        // EL has methods CL doesn't support
85        let mut missing_in_cl: Vec<_> =
86            self.inner.iter().filter(|cap| !cl_set.contains(cap.as_str())).cloned().collect();
87        missing_in_cl.sort_unstable();
88
89        CapabilityMismatches { missing_in_el, missing_in_cl }
90    }
91
92    /// Logs warnings if CL and EL capabilities don't match for critical methods.
93    ///
94    /// Called during `engine_exchangeCapabilities` to warn operators about
95    /// version mismatches between the consensus layer and execution layer.
96    ///
97    /// Only warns about critical methods (`engine_forkchoiceUpdated`, `engine_getPayload`,
98    /// `engine_newPayload`) that are essential for block production and chain synchronization.
99    /// Non-critical methods like `engine_getBlobs` are not warned about since not all
100    /// clients support them.
101    pub fn log_capability_mismatches(&self, cl_capabilities: &[String]) {
102        let mismatches = self.get_capability_mismatches(cl_capabilities);
103
104        let critical_missing_in_el: Vec<_> = mismatches
105            .missing_in_el
106            .iter()
107            .filter(|m| should_warn_for_method(m))
108            .cloned()
109            .collect();
110
111        let critical_missing_in_cl: Vec<_> = mismatches
112            .missing_in_cl
113            .iter()
114            .filter(|m| should_warn_for_method(m))
115            .cloned()
116            .collect();
117
118        if !critical_missing_in_el.is_empty() {
119            warn!(
120                target: "rpc::engine",
121                missing = ?critical_missing_in_el,
122                "CL supports Engine API methods that Reth doesn't. Consider upgrading Reth."
123            );
124        }
125
126        if !critical_missing_in_cl.is_empty() {
127            warn!(
128                target: "rpc::engine",
129                missing = ?critical_missing_in_cl,
130                "Reth supports Engine API methods that CL doesn't. Consider upgrading your consensus client."
131            );
132        }
133    }
134}
135
136/// Returns `true` if the method is critical for block production and chain synchronization.
137fn is_critical_method(method: &str) -> bool {
138    CRITICAL_METHOD_PREFIXES.iter().any(|prefix| {
139        method.starts_with(prefix) &&
140            method[prefix.len()..]
141                .strip_prefix('V')
142                .is_some_and(|s| s.chars().next().is_some_and(|c| c.is_ascii_digit()))
143    })
144}
145
146/// Returns `true` if the method should warn on a capability mismatch.
147fn should_warn_for_method(method: &str) -> bool {
148    is_critical_method(method) && !is_unstable_method(method)
149}
150
151/// Returns `true` if the method belongs to an upcoming, unstable Engine API fork.
152fn is_unstable_method(method: &str) -> bool {
153    UNSTABLE_METHODS.contains(&method)
154}
155
156impl Default for EngineCapabilities {
157    fn default() -> Self {
158        Self::new(CAPABILITIES.iter().copied())
159    }
160}
161
162/// Result of comparing CL and EL capabilities.
163#[derive(Debug, Default, PartialEq, Eq)]
164pub struct CapabilityMismatches {
165    /// Methods supported by CL but not by EL (Reth).
166    /// Operators should consider upgrading Reth.
167    pub missing_in_el: Vec<String>,
168    /// Methods supported by EL (Reth) but not by CL.
169    /// Operators should consider upgrading their consensus client.
170    pub missing_in_cl: Vec<String>,
171}
172
173impl CapabilityMismatches {
174    /// Returns `true` if there are no mismatches.
175    pub const fn is_empty(&self) -> bool {
176        self.missing_in_el.is_empty() && self.missing_in_cl.is_empty()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_no_mismatches() {
186        let el = EngineCapabilities::new(["method_a", "method_b"]);
187        let cl = vec!["method_a".to_string(), "method_b".to_string()];
188
189        let result = el.get_capability_mismatches(&cl);
190        assert!(result.is_empty());
191    }
192
193    #[test]
194    fn test_cl_has_extra_methods() {
195        let el = EngineCapabilities::new(["method_a"]);
196        let cl = vec!["method_a".to_string(), "method_b".to_string()];
197
198        let result = el.get_capability_mismatches(&cl);
199        assert_eq!(result.missing_in_el, vec!["method_b"]);
200        assert!(result.missing_in_cl.is_empty());
201    }
202
203    #[test]
204    fn test_el_has_extra_methods() {
205        let el = EngineCapabilities::new(["method_a", "method_b"]);
206        let cl = vec!["method_a".to_string()];
207
208        let result = el.get_capability_mismatches(&cl);
209        assert!(result.missing_in_el.is_empty());
210        assert_eq!(result.missing_in_cl, vec!["method_b"]);
211    }
212
213    #[test]
214    fn test_both_have_extra_methods() {
215        let el = EngineCapabilities::new(["method_a", "method_c"]);
216        let cl = vec!["method_a".to_string(), "method_b".to_string()];
217
218        let result = el.get_capability_mismatches(&cl);
219        assert_eq!(result.missing_in_el, vec!["method_b"]);
220        assert_eq!(result.missing_in_cl, vec!["method_c"]);
221    }
222
223    #[test]
224    fn test_results_are_sorted() {
225        let el = EngineCapabilities::new(["z_method", "a_method"]);
226        let cl = vec!["z_other".to_string(), "a_other".to_string()];
227
228        let result = el.get_capability_mismatches(&cl);
229        assert_eq!(result.missing_in_el, vec!["a_other", "z_other"]);
230        assert_eq!(result.missing_in_cl, vec!["a_method", "z_method"]);
231    }
232
233    #[test]
234    fn test_is_critical_method() {
235        assert!(is_critical_method("engine_forkchoiceUpdatedV1"));
236        assert!(is_critical_method("engine_forkchoiceUpdatedV3"));
237        assert!(is_critical_method("engine_forkchoiceUpdatedV4"));
238        assert!(is_critical_method("engine_getPayloadV1"));
239        assert!(is_critical_method("engine_getPayloadV4"));
240        assert!(is_critical_method("engine_getPayloadV6"));
241        assert!(is_critical_method("engine_newPayloadV1"));
242        assert!(is_critical_method("engine_newPayloadV4"));
243        assert!(is_critical_method("engine_newPayloadV5"));
244
245        assert!(!is_critical_method("engine_getBlobsV1"));
246        assert!(!is_critical_method("engine_getBlobsV3"));
247        assert!(!is_critical_method("engine_getBlobsV4"));
248        assert!(!is_critical_method("engine_getPayloadBodiesByHashV1"));
249        assert!(!is_critical_method("engine_getPayloadBodiesByRangeV1"));
250        assert!(!is_critical_method("engine_getClientVersionV1"));
251    }
252
253    #[test]
254    fn test_unstable_methods_do_not_warn() {
255        assert!(!should_warn_for_method("engine_forkchoiceUpdatedV4"));
256        assert!(!should_warn_for_method("engine_getPayloadV6"));
257        assert!(!should_warn_for_method("engine_newPayloadV5"));
258
259        assert!(should_warn_for_method("engine_forkchoiceUpdatedV3"));
260        assert!(should_warn_for_method("engine_getPayloadV5"));
261        assert!(should_warn_for_method("engine_newPayloadV4"));
262
263        assert!(!should_warn_for_method("engine_getBlobsV4"));
264    }
265}