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