mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-02-19 03:04:27 -05:00
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:
@@ -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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user