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