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