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