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
13pub 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#[derive(Debug, Clone)]
41pub struct EngineCapabilities {
42 inner: HashSet<String>,
43}
44
45impl EngineCapabilities {
46 pub fn new(capabilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
48 Self { inner: capabilities.into_iter().map(Into::into).collect() }
49 }
50
51 pub fn list(&self) -> Vec<String> {
53 self.inner.iter().cloned().collect()
54 }
55
56 pub const fn as_set(&self) -> &HashSet<String> {
58 &self.inner
59 }
60
61 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 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 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 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
120fn 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#[derive(Debug, Default, PartialEq, Eq)]
138pub struct CapabilityMismatches {
139 pub missing_in_el: Vec<String>,
142 pub missing_in_cl: Vec<String>,
145}
146
147impl CapabilityMismatches {
148 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}