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",
16 "engine_forkchoiceUpdatedV5",
17 "engine_getPayloadV6",
18 "engine_newPayloadV5",
19 "engine_newPayloadV6",
20];
21
22pub 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#[derive(Debug, Clone)]
58pub struct EngineCapabilities {
59 inner: HashSet<String>,
60}
61
62impl EngineCapabilities {
63 pub fn new(capabilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
65 Self { inner: capabilities.into_iter().map(Into::into).collect() }
66 }
67
68 pub fn list(&self) -> Vec<String> {
70 self.inner.iter().cloned().collect()
71 }
72
73 pub const fn as_set(&self) -> &HashSet<String> {
75 &self.inner
76 }
77
78 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 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 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 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
145fn 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
155fn should_warn_for_method(method: &str) -> bool {
157 is_critical_method(method) && !is_unstable_method(method)
158}
159
160fn 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#[derive(Debug, Default, PartialEq, Eq)]
173pub struct CapabilityMismatches {
174 pub missing_in_el: Vec<String>,
177 pub missing_in_cl: Vec<String>,
180}
181
182impl CapabilityMismatches {
183 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}