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_forkchoiceUpdatedV4",
21 "engine_getClientVersionV1",
22 "engine_getPayloadV1",
23 "engine_getPayloadV2",
24 "engine_getPayloadV3",
25 "engine_getPayloadV4",
26 "engine_getPayloadV5",
27 "engine_getPayloadV6",
28 "engine_newPayloadV1",
29 "engine_newPayloadV2",
30 "engine_newPayloadV3",
31 "engine_newPayloadV4",
32 "engine_newPayloadV5",
33 "engine_getPayloadBodiesByHashV1",
34 "engine_getPayloadBodiesByHashV2",
35 "engine_getPayloadBodiesByRangeV1",
36 "engine_getPayloadBodiesByRangeV2",
37 "engine_getBlobsV1",
38 "engine_getBlobsV2",
39 "engine_getBlobsV3",
40 "engine_getBlobsV4",
41];
42
43#[derive(Debug, Clone)]
45pub struct EngineCapabilities {
46 inner: HashSet<String>,
47}
48
49impl EngineCapabilities {
50 pub fn new(capabilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
52 Self { inner: capabilities.into_iter().map(Into::into).collect() }
53 }
54
55 pub fn list(&self) -> Vec<String> {
57 self.inner.iter().cloned().collect()
58 }
59
60 pub const fn as_set(&self) -> &HashSet<String> {
62 &self.inner
63 }
64
65 pub fn get_capability_mismatches(&self, cl_capabilities: &[String]) -> CapabilityMismatches {
70 let cl_set: HashSet<&str> = cl_capabilities.iter().map(String::as_str).collect();
71
72 let mut missing_in_el: Vec<_> = cl_capabilities
74 .iter()
75 .filter(|cap| !self.inner.contains(cap.as_str()))
76 .cloned()
77 .collect();
78 missing_in_el.sort_unstable();
79
80 let mut missing_in_cl: Vec<_> =
82 self.inner.iter().filter(|cap| !cl_set.contains(cap.as_str())).cloned().collect();
83 missing_in_cl.sort_unstable();
84
85 CapabilityMismatches { missing_in_el, missing_in_cl }
86 }
87
88 pub fn log_capability_mismatches(&self, cl_capabilities: &[String]) {
98 let mismatches = self.get_capability_mismatches(cl_capabilities);
99
100 let critical_missing_in_el: Vec<_> =
101 mismatches.missing_in_el.iter().filter(|m| is_critical_method(m)).cloned().collect();
102
103 let critical_missing_in_cl: Vec<_> =
104 mismatches.missing_in_cl.iter().filter(|m| is_critical_method(m)).cloned().collect();
105
106 if !critical_missing_in_el.is_empty() {
107 warn!(
108 target: "rpc::engine",
109 missing = ?critical_missing_in_el,
110 "CL supports Engine API methods that Reth doesn't. Consider upgrading Reth."
111 );
112 }
113
114 if !critical_missing_in_cl.is_empty() {
115 warn!(
116 target: "rpc::engine",
117 missing = ?critical_missing_in_cl,
118 "Reth supports Engine API methods that CL doesn't. Consider upgrading your consensus client."
119 );
120 }
121 }
122}
123
124fn is_critical_method(method: &str) -> bool {
126 CRITICAL_METHOD_PREFIXES.iter().any(|prefix| {
127 method.starts_with(prefix) &&
128 method[prefix.len()..]
129 .strip_prefix('V')
130 .is_some_and(|s| s.chars().next().is_some_and(|c| c.is_ascii_digit()))
131 })
132}
133
134impl Default for EngineCapabilities {
135 fn default() -> Self {
136 Self::new(CAPABILITIES.iter().copied())
137 }
138}
139
140#[derive(Debug, Default, PartialEq, Eq)]
142pub struct CapabilityMismatches {
143 pub missing_in_el: Vec<String>,
146 pub missing_in_cl: Vec<String>,
149}
150
151impl CapabilityMismatches {
152 pub const fn is_empty(&self) -> bool {
154 self.missing_in_el.is_empty() && self.missing_in_cl.is_empty()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_no_mismatches() {
164 let el = EngineCapabilities::new(["method_a", "method_b"]);
165 let cl = vec!["method_a".to_string(), "method_b".to_string()];
166
167 let result = el.get_capability_mismatches(&cl);
168 assert!(result.is_empty());
169 }
170
171 #[test]
172 fn test_cl_has_extra_methods() {
173 let el = EngineCapabilities::new(["method_a"]);
174 let cl = vec!["method_a".to_string(), "method_b".to_string()];
175
176 let result = el.get_capability_mismatches(&cl);
177 assert_eq!(result.missing_in_el, vec!["method_b"]);
178 assert!(result.missing_in_cl.is_empty());
179 }
180
181 #[test]
182 fn test_el_has_extra_methods() {
183 let el = EngineCapabilities::new(["method_a", "method_b"]);
184 let cl = vec!["method_a".to_string()];
185
186 let result = el.get_capability_mismatches(&cl);
187 assert!(result.missing_in_el.is_empty());
188 assert_eq!(result.missing_in_cl, vec!["method_b"]);
189 }
190
191 #[test]
192 fn test_both_have_extra_methods() {
193 let el = EngineCapabilities::new(["method_a", "method_c"]);
194 let cl = vec!["method_a".to_string(), "method_b".to_string()];
195
196 let result = el.get_capability_mismatches(&cl);
197 assert_eq!(result.missing_in_el, vec!["method_b"]);
198 assert_eq!(result.missing_in_cl, vec!["method_c"]);
199 }
200
201 #[test]
202 fn test_results_are_sorted() {
203 let el = EngineCapabilities::new(["z_method", "a_method"]);
204 let cl = vec!["z_other".to_string(), "a_other".to_string()];
205
206 let result = el.get_capability_mismatches(&cl);
207 assert_eq!(result.missing_in_el, vec!["a_other", "z_other"]);
208 assert_eq!(result.missing_in_cl, vec!["a_method", "z_method"]);
209 }
210
211 #[test]
212 fn test_is_critical_method() {
213 assert!(is_critical_method("engine_forkchoiceUpdatedV1"));
214 assert!(is_critical_method("engine_forkchoiceUpdatedV3"));
215 assert!(is_critical_method("engine_getPayloadV1"));
216 assert!(is_critical_method("engine_getPayloadV4"));
217 assert!(is_critical_method("engine_newPayloadV1"));
218 assert!(is_critical_method("engine_newPayloadV4"));
219
220 assert!(!is_critical_method("engine_getBlobsV1"));
221 assert!(!is_critical_method("engine_getBlobsV3"));
222 assert!(!is_critical_method("engine_getBlobsV4"));
223 assert!(!is_critical_method("engine_getPayloadBodiesByHashV1"));
224 assert!(!is_critical_method("engine_getPayloadBodiesByRangeV1"));
225 assert!(!is_critical_method("engine_getClientVersionV1"));
226 }
227}