feat: Check CL/Reth capability compatibility (#20348)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
tonis
2026-01-20 21:19:31 +07:00
committed by GitHub
parent 346cc0da71
commit a0845bab18
2 changed files with 139 additions and 22 deletions

View File

@@ -1,6 +1,11 @@
use std::collections::HashSet;
//! Engine API capabilities.
/// The list of all supported Engine capabilities available over the engine endpoint.
use std::collections::HashSet;
use tracing::warn;
/// All Engine API capabilities supported by Reth (Ethereum mainnet).
///
/// See <https://github.com/ethereum/execution-apis/tree/main/src/engine> for updates.
pub const CAPABILITIES: &[&str] = &[
"engine_forkchoiceUpdatedV1",
"engine_forkchoiceUpdatedV2",
@@ -22,43 +27,150 @@ pub const CAPABILITIES: &[&str] = &[
"engine_getBlobsV3",
];
// The list of all supported Engine capabilities available over the engine endpoint.
///
/// Latest spec: Prague
/// Engine API capabilities set.
#[derive(Debug, Clone)]
pub struct EngineCapabilities {
inner: HashSet<String>,
}
impl EngineCapabilities {
/// Creates a new `EngineCapabilities` instance with the given capabilities.
pub fn new(capabilities: impl IntoIterator<Item: Into<String>>) -> Self {
/// Creates from an iterator of capability strings.
pub fn new(capabilities: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self { inner: capabilities.into_iter().map(Into::into).collect() }
}
/// Returns the list of all supported Engine capabilities for Prague spec.
fn prague() -> Self {
Self { inner: CAPABILITIES.iter().copied().map(str::to_owned).collect() }
}
/// Returns the list of all supported Engine capabilities.
/// Returns the capabilities as a list of strings.
pub fn list(&self) -> Vec<String> {
self.inner.iter().cloned().collect()
}
/// Inserts a new capability.
pub fn add_capability(&mut self, capability: impl Into<String>) {
self.inner.insert(capability.into());
/// Returns a reference to the inner set.
pub const fn as_set(&self) -> &HashSet<String> {
&self.inner
}
/// Removes a capability.
pub fn remove_capability(&mut self, capability: &str) -> Option<String> {
self.inner.take(capability)
/// Compares CL capabilities with this EL's capabilities and returns any mismatches.
///
/// Called during `engine_exchangeCapabilities` to detect version mismatches
/// between the consensus layer and execution layer.
pub fn get_capability_mismatches(&self, cl_capabilities: &[String]) -> CapabilityMismatches {
let cl_set: HashSet<&str> = cl_capabilities.iter().map(String::as_str).collect();
// CL has methods EL doesn't support
let mut missing_in_el: Vec<_> = cl_capabilities
.iter()
.filter(|cap| !self.inner.contains(cap.as_str()))
.cloned()
.collect();
missing_in_el.sort();
// EL has methods CL doesn't support
let mut missing_in_cl: Vec<_> =
self.inner.iter().filter(|cap| !cl_set.contains(cap.as_str())).cloned().collect();
missing_in_cl.sort();
CapabilityMismatches { missing_in_el, missing_in_cl }
}
/// Logs warnings if CL and EL capabilities don't match.
///
/// Called during `engine_exchangeCapabilities` to warn operators about
/// version mismatches between the consensus layer and execution layer.
pub fn log_capability_mismatches(&self, cl_capabilities: &[String]) {
let mismatches = self.get_capability_mismatches(cl_capabilities);
if !mismatches.missing_in_el.is_empty() {
warn!(
target: "rpc::engine",
missing = ?mismatches.missing_in_el,
"CL supports Engine API methods that Reth doesn't. Consider upgrading Reth."
);
}
if !mismatches.missing_in_cl.is_empty() {
warn!(
target: "rpc::engine",
missing = ?mismatches.missing_in_cl,
"Reth supports Engine API methods that CL doesn't. Consider upgrading your consensus client."
);
}
}
}
impl Default for EngineCapabilities {
fn default() -> Self {
Self::prague()
Self::new(CAPABILITIES.iter().copied())
}
}
/// Result of comparing CL and EL capabilities.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct CapabilityMismatches {
/// Methods supported by CL but not by EL (Reth).
/// Operators should consider upgrading Reth.
pub missing_in_el: Vec<String>,
/// Methods supported by EL (Reth) but not by CL.
/// Operators should consider upgrading their consensus client.
pub missing_in_cl: Vec<String>,
}
impl CapabilityMismatches {
/// Returns `true` if there are no mismatches.
pub const fn is_empty(&self) -> bool {
self.missing_in_el.is_empty() && self.missing_in_cl.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_mismatches() {
let el = EngineCapabilities::new(["method_a", "method_b"]);
let cl = vec!["method_a".to_string(), "method_b".to_string()];
let result = el.get_capability_mismatches(&cl);
assert!(result.is_empty());
}
#[test]
fn test_cl_has_extra_methods() {
let el = EngineCapabilities::new(["method_a"]);
let cl = vec!["method_a".to_string(), "method_b".to_string()];
let result = el.get_capability_mismatches(&cl);
assert_eq!(result.missing_in_el, vec!["method_b"]);
assert!(result.missing_in_cl.is_empty());
}
#[test]
fn test_el_has_extra_methods() {
let el = EngineCapabilities::new(["method_a", "method_b"]);
let cl = vec!["method_a".to_string()];
let result = el.get_capability_mismatches(&cl);
assert!(result.missing_in_el.is_empty());
assert_eq!(result.missing_in_cl, vec!["method_b"]);
}
#[test]
fn test_both_have_extra_methods() {
let el = EngineCapabilities::new(["method_a", "method_c"]);
let cl = vec!["method_a".to_string(), "method_b".to_string()];
let result = el.get_capability_mismatches(&cl);
assert_eq!(result.missing_in_el, vec!["method_b"]);
assert_eq!(result.missing_in_cl, vec!["method_c"]);
}
#[test]
fn test_results_are_sorted() {
let el = EngineCapabilities::new(["z_method", "a_method"]);
let cl = vec!["z_other".to_string(), "a_other".to_string()];
let result = el.get_capability_mismatches(&cl);
assert_eq!(result.missing_in_el, vec!["a_other", "z_other"]);
assert_eq!(result.missing_in_cl, vec!["a_method", "z_method"]);
}
}

View File

@@ -1134,8 +1134,13 @@ where
/// Handler for `engine_exchangeCapabilitiesV1`
/// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/common.md#capabilities>
async fn exchange_capabilities(&self, _capabilities: Vec<String>) -> RpcResult<Vec<String>> {
Ok(self.capabilities().list())
async fn exchange_capabilities(&self, capabilities: Vec<String>) -> RpcResult<Vec<String>> {
trace!(target: "rpc::engine", "Serving engine_exchangeCapabilities");
let el_caps = self.capabilities();
el_caps.log_capability_mismatches(&capabilities);
Ok(el_caps.list())
}
async fn get_blobs_v1(