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 "engine_hasBlobs",
46];
47
48#[derive(Debug, Clone)]
50pub struct EngineCapabilities {
51 inner: HashSet<String>,
52}
53
54impl EngineCapabilities {
55 pub fn new(capabilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
57 Self { inner: capabilities.into_iter().map(Into::into).collect() }
58 }
59
60 pub fn list(&self) -> Vec<String> {
62 self.inner.iter().cloned().collect()
63 }
64
65 pub const fn as_set(&self) -> &HashSet<String> {
67 &self.inner
68 }
69
70 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 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 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 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
137fn 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
147fn should_warn_for_method(method: &str) -> bool {
149 is_critical_method(method) && !is_unstable_method(method)
150}
151
152fn 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#[derive(Debug, Default, PartialEq, Eq)]
165pub struct CapabilityMismatches {
166 pub missing_in_el: Vec<String>,
169 pub missing_in_cl: Vec<String>,
172}
173
174impl CapabilityMismatches {
175 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}