reth_rpc_engine_api/
capabilities.rs1use std::collections::HashSet;
4use tracing::warn;
5
6const CRITICAL_METHOD_PREFIXES: &[&str] =
11 &["engine_forkchoiceUpdated", "engine_getPayload", "engine_newPayload"];
12
13const UNSTABLE_METHODS: &[&str] =
15 &["engine_forkchoiceUpdatedV4", "engine_getPayloadV6", "engine_newPayloadV5"];
16
17pub 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#[derive(Debug, Clone)]
49pub struct EngineCapabilities {
50 inner: HashSet<String>,
51}
52
53impl EngineCapabilities {
54 pub fn new(capabilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
56 Self { inner: capabilities.into_iter().map(Into::into).collect() }
57 }
58
59 pub fn list(&self) -> Vec<String> {
61 self.inner.iter().cloned().collect()
62 }
63
64 pub const fn as_set(&self) -> &HashSet<String> {
66 &self.inner
67 }
68
69 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 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 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 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
136fn 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
146fn should_warn_for_method(method: &str) -> bool {
148 is_critical_method(method) && !is_unstable_method(method)
149}
150
151fn 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#[derive(Debug, Default, PartialEq, Eq)]
164pub struct CapabilityMismatches {
165 pub missing_in_el: Vec<String>,
168 pub missing_in_cl: Vec<String>,
171}
172
173impl CapabilityMismatches {
174 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}