Serve Bellatrix preset in BN API (#3425)

## Issue Addressed

Resolves #3388
Resolves #2638

## Proposed Changes

- Return the `BellatrixPreset` on `/eth/v1/config/spec` by default.
- Allow users to opt out of this by providing `--http-spec-fork=altair` (unless there's a Bellatrix fork epoch set).
- Add the Altair constants from #2638 and make serving the constants non-optional (the `http-disable-legacy-spec` flag is deprecated).
- Modify the VC to only read the `Config` and not to log extra fields. This prevents it from having to muck around parsing the `ConfigAndPreset` fields it doesn't need.

## Additional Info

This change is backwards-compatible for the VC and the BN, but is marked as a breaking change for the removal of `--http-disable-legacy-spec`.

I tried making `Config` a `superstruct` too, but getting the automatic decoding to work was a huge pain and was going to require a lot of hacks, so I gave up in favour of keeping the default-based approach we have now.
This commit is contained in:
Michael Sproul 2022-08-10 07:52:59 +00:00
parent c25934956b
commit 4e05f19fb5
19 changed files with 167 additions and 142 deletions

1
Cargo.lock generated
View File

@ -7073,6 +7073,7 @@ dependencies = [
"itertools",
"lazy_static",
"log",
"maplit",
"parking_lot 0.12.1",
"rand 0.8.5",
"rand_xorshift",

View File

@ -132,8 +132,7 @@ pub fn test_spec<E: EthSpec>() -> ChainSpec {
FORK_NAME_ENV_VAR, e
)
});
let fork = ForkName::from_str(fork_name.as_str())
.unwrap_or_else(|()| panic!("unknown FORK_NAME: {}", fork_name));
let fork = ForkName::from_str(fork_name.as_str()).unwrap();
fork.make_genesis_spec(E::default_spec())
} else {
E::default_spec()

View File

@ -104,9 +104,9 @@ pub struct Config {
pub listen_addr: IpAddr,
pub listen_port: u16,
pub allow_origin: Option<String>,
pub serve_legacy_spec: bool,
pub tls_config: Option<TlsConfig>,
pub allow_sync_stalled: bool,
pub spec_fork_name: Option<ForkName>,
}
impl Default for Config {
@ -116,9 +116,9 @@ impl Default for Config {
listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
listen_port: 5052,
allow_origin: None,
serve_legacy_spec: true,
tls_config: None,
allow_sync_stalled: false,
spec_fork_name: None,
}
}
}
@ -1534,18 +1534,15 @@ pub fn serve<T: BeaconChainTypes>(
});
// GET config/spec
let serve_legacy_spec = ctx.config.serve_legacy_spec;
let spec_fork_name = ctx.config.spec_fork_name;
let get_config_spec = config_path
.and(warp::path("spec"))
.and(warp::path::end())
.and(chain_filter.clone())
.and_then(move |chain: Arc<BeaconChain<T>>| {
blocking_json_task(move || {
let mut config_and_preset =
ConfigAndPreset::from_chain_spec::<T::EthSpec>(&chain.spec);
if serve_legacy_spec {
config_and_preset.make_backwards_compat(&chain.spec);
}
let config_and_preset =
ConfigAndPreset::from_chain_spec::<T::EthSpec>(&chain.spec, spec_fork_name);
Ok(api_types::GenericResponse::from(config_and_preset))
})
});

View File

@ -141,9 +141,9 @@ pub async fn create_api_server_on_port<T: BeaconChainTypes>(
listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
listen_port: port,
allow_origin: None,
serve_legacy_spec: true,
tls_config: None,
allow_sync_stalled: false,
spec_fork_name: None,
},
chain: Some(chain.clone()),
network_tx: Some(network_tx),

View File

@ -1253,10 +1253,13 @@ impl ApiTester {
}
pub async fn test_get_config_spec(self) -> Self {
let result = self.client.get_config_spec().await.unwrap().data;
let mut expected = ConfigAndPreset::from_chain_spec::<E>(&self.chain.spec);
expected.make_backwards_compat(&self.chain.spec);
let result = self
.client
.get_config_spec::<ConfigAndPresetBellatrix>()
.await
.map(|res| ConfigAndPreset::Bellatrix(res.data))
.unwrap();
let expected = ConfigAndPreset::from_chain_spec::<E>(&self.chain.spec, None);
assert_eq!(result, expected);

View File

@ -229,8 +229,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.arg(
Arg::with_name("http-disable-legacy-spec")
.long("http-disable-legacy-spec")
.help("Disable serving of legacy data on the /config/spec endpoint. May be \
disabled by default in a future release.")
.hidden(true)
)
.arg(
Arg::with_name("http-spec-fork")
.long("http-spec-fork")
.help("Serve the spec for a specific hard fork on /eth/v1/config/spec. It should \
not be necessary to set this flag.")
.takes_value(true)
)
.arg(
Arg::with_name("http-enable-tls")

View File

@ -116,7 +116,14 @@ pub fn get_config<E: EthSpec>(
}
if cli_args.is_present("http-disable-legacy-spec") {
client_config.http_api.serve_legacy_spec = false;
warn!(
log,
"The flag --http-disable-legacy-spec is deprecated and will be removed"
);
}
if let Some(fork_name) = clap_utils::parse_optional(cli_args, "http-spec-fork")? {
client_config.http_api.spec_fork_name = Some(fork_name);
}
if cli_args.is_present("http-enable-tls") {

View File

@ -977,7 +977,9 @@ impl BeaconNodeHttpClient {
}
/// `GET config/spec`
pub async fn get_config_spec(&self) -> Result<GenericResponse<ConfigAndPreset>, Error> {
pub async fn get_config_spec<T: Serialize + DeserializeOwned>(
&self,
) -> Result<GenericResponse<T>, Error> {
let mut path = self.eth_path(V1)?;
path.path_segments_mut()

View File

@ -354,7 +354,9 @@ impl ValidatorClientHttpClient {
}
/// `GET lighthouse/spec`
pub async fn get_lighthouse_spec(&self) -> Result<GenericResponse<ConfigAndPreset>, Error> {
pub async fn get_lighthouse_spec<T: Serialize + DeserializeOwned>(
&self,
) -> Result<GenericResponse<T>, Error> {
let mut path = self.server.full.clone();
path.path_segments_mut()

View File

@ -21,17 +21,17 @@ impl ResponseOptional for Result<Response, Error> {
/// Trait for extracting the fork name from the headers of a response.
pub trait ResponseForkName {
#[allow(clippy::result_unit_err)]
fn fork_name_from_header(&self) -> Result<Option<ForkName>, ()>;
fn fork_name_from_header(&self) -> Result<Option<ForkName>, String>;
}
impl ResponseForkName for Response {
fn fork_name_from_header(&self) -> Result<Option<ForkName>, ()> {
fn fork_name_from_header(&self) -> Result<Option<ForkName>, String> {
self.headers()
.get(CONSENSUS_VERSION_HEADER)
.map(|fork_name| {
fork_name
.to_str()
.map_err(|_| ())
.map_err(|e| e.to_string())
.and_then(ForkName::from_str)
})
.transpose()

View File

@ -47,6 +47,7 @@ superstruct = "0.5.0"
serde_json = "1.0.74"
smallvec = "1.8.0"
serde_with = "1.13.0"
maplit = "1.0.2"
[dev-dependencies]
criterion = "0.3.3"

View File

@ -803,6 +803,10 @@ impl Default for ChainSpec {
}
/// Exact implementation of the *config* object from the Ethereum spec (YAML/JSON).
///
/// Fields relevant to hard forks after Altair should be optional so that we can continue
/// to parse Altair configs. This default approach turns out to be much simpler than trying to
/// make `Config` a superstruct because of the hassle of deserializing an untagged enum.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub struct Config {
@ -813,17 +817,13 @@ pub struct Config {
#[serde(default)]
pub preset_base: String,
// TODO(merge): remove this default
#[serde(default = "default_terminal_total_difficulty")]
#[serde(with = "eth2_serde_utils::quoted_u256")]
pub terminal_total_difficulty: Uint256,
// TODO(merge): remove this default
#[serde(default = "default_terminal_block_hash")]
pub terminal_block_hash: ExecutionBlockHash,
// TODO(merge): remove this default
#[serde(default = "default_terminal_block_hash_activation_epoch")]
pub terminal_block_hash_activation_epoch: Epoch,
// TODO(merge): remove this default
#[serde(default = "default_safe_slots_to_import_optimistically")]
#[serde(with = "eth2_serde_utils::quoted_u64")]
pub safe_slots_to_import_optimistically: u64,
@ -843,12 +843,10 @@ pub struct Config {
#[serde(deserialize_with = "deserialize_fork_epoch")]
pub altair_fork_epoch: Option<MaybeQuoted<Epoch>>,
// TODO(merge): remove this default
#[serde(default = "default_bellatrix_fork_version")]
#[serde(with = "eth2_serde_utils::bytes_4_hex")]
bellatrix_fork_version: [u8; 4],
// TODO(merge): remove this default
#[serde(default = "default_bellatrix_fork_epoch")]
#[serde(default)]
#[serde(serialize_with = "serialize_fork_epoch")]
#[serde(deserialize_with = "deserialize_fork_epoch")]
pub bellatrix_fork_epoch: Option<MaybeQuoted<Epoch>>,
@ -890,10 +888,6 @@ fn default_bellatrix_fork_version() -> [u8; 4] {
[0xff, 0xff, 0xff, 0xff]
}
fn default_bellatrix_fork_epoch() -> Option<MaybeQuoted<Epoch>> {
None
}
/// Placeholder value: 2^256-2^10 (115792089237316195423570985008687907853269984665640564039457584007913129638912).
///
/// Taken from https://github.com/ethereum/consensus-specs/blob/d5e4828aecafaf1c57ef67a5f23c4ae7b08c5137/configs/mainnet.yaml#L15-L16
@ -1335,10 +1329,7 @@ mod yaml_tests {
default_safe_slots_to_import_optimistically()
);
assert_eq!(
chain_spec.bellatrix_fork_epoch,
default_bellatrix_fork_epoch()
);
assert_eq!(chain_spec.bellatrix_fork_epoch, None);
assert_eq!(
chain_spec.bellatrix_fork_version,

View File

@ -1,12 +1,21 @@
use crate::{AltairPreset, BasePreset, BellatrixPreset, ChainSpec, Config, EthSpec};
use crate::{
consts::altair, AltairPreset, BasePreset, BellatrixPreset, ChainSpec, Config, EthSpec, ForkName,
};
use maplit::hashmap;
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use superstruct::superstruct;
/// Fusion of a runtime-config with the compile-time preset values.
///
/// Mostly useful for the API.
#[superstruct(
variants(Altair, Bellatrix),
variant_attributes(derive(Serialize, Deserialize, Debug, PartialEq, Clone))
)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(untagged)]
pub struct ConfigAndPreset {
#[serde(flatten)]
pub config: Config,
@ -15,80 +24,75 @@ pub struct ConfigAndPreset {
pub base_preset: BasePreset,
#[serde(flatten)]
pub altair_preset: AltairPreset,
// TODO(merge): re-enable
// #[serde(flatten)]
// pub bellatrix_preset: BellatrixPreset,
#[superstruct(only(Bellatrix))]
#[serde(flatten)]
pub bellatrix_preset: BellatrixPreset,
/// The `extra_fields` map allows us to gracefully decode fields intended for future hard forks.
#[serde(flatten)]
pub extra_fields: HashMap<String, Value>,
}
impl ConfigAndPreset {
pub fn from_chain_spec<T: EthSpec>(spec: &ChainSpec) -> Self {
pub fn from_chain_spec<T: EthSpec>(spec: &ChainSpec, fork_name: Option<ForkName>) -> Self {
let config = Config::from_chain_spec::<T>(spec);
let base_preset = BasePreset::from_chain_spec::<T>(spec);
let altair_preset = AltairPreset::from_chain_spec::<T>(spec);
// TODO(merge): re-enable
let _bellatrix_preset = BellatrixPreset::from_chain_spec::<T>(spec);
let extra_fields = HashMap::new();
let extra_fields = get_extra_fields(spec);
Self {
config,
base_preset,
altair_preset,
extra_fields,
if spec.bellatrix_fork_epoch.is_some()
|| fork_name == None
|| fork_name == Some(ForkName::Merge)
{
let bellatrix_preset = BellatrixPreset::from_chain_spec::<T>(spec);
ConfigAndPreset::Bellatrix(ConfigAndPresetBellatrix {
config,
base_preset,
altair_preset,
bellatrix_preset,
extra_fields,
})
} else {
ConfigAndPreset::Altair(ConfigAndPresetAltair {
config,
base_preset,
altair_preset,
extra_fields,
})
}
}
}
/// Add fields that were previously part of the config but are now constants.
pub fn make_backwards_compat(&mut self, spec: &ChainSpec) {
let hex_string = |value: &[u8]| format!("0x{}", hex::encode(&value));
let u32_hex = |v: u32| hex_string(&v.to_le_bytes());
let u8_hex = |v: u8| hex_string(&v.to_le_bytes());
let fields = vec![
(
"bls_withdrawal_prefix",
u8_hex(spec.bls_withdrawal_prefix_byte),
),
(
"domain_beacon_proposer",
u32_hex(spec.domain_beacon_proposer),
),
(
"domain_beacon_attester",
u32_hex(spec.domain_beacon_attester),
),
("domain_randao", u32_hex(spec.domain_randao)),
("domain_deposit", u32_hex(spec.domain_deposit)),
("domain_voluntary_exit", u32_hex(spec.domain_voluntary_exit)),
(
"domain_selection_proof",
u32_hex(spec.domain_selection_proof),
),
(
"domain_aggregate_and_proof",
u32_hex(spec.domain_aggregate_and_proof),
),
(
"domain_application_mask",
u32_hex(spec.domain_application_mask),
),
(
"target_aggregators_per_committee",
spec.target_aggregators_per_committee.to_string(),
),
(
"random_subnets_per_validator",
spec.random_subnets_per_validator.to_string(),
),
(
"epochs_per_random_subnet_subscription",
spec.epochs_per_random_subnet_subscription.to_string(),
),
];
for (key, value) in fields {
self.extra_fields.insert(key.to_uppercase(), value.into());
}
/// Get a hashmap of constants to add to the `PresetAndConfig`
pub fn get_extra_fields(spec: &ChainSpec) -> HashMap<String, Value> {
let hex_string = |value: &[u8]| format!("0x{}", hex::encode(&value)).into();
let u32_hex = |v: u32| hex_string(&v.to_le_bytes());
let u8_hex = |v: u8| hex_string(&v.to_le_bytes());
hashmap! {
"bls_withdrawal_prefix".to_uppercase() => u8_hex(spec.bls_withdrawal_prefix_byte),
"domain_beacon_proposer".to_uppercase() => u32_hex(spec.domain_beacon_proposer),
"domain_beacon_attester".to_uppercase() => u32_hex(spec.domain_beacon_attester),
"domain_randao".to_uppercase()=> u32_hex(spec.domain_randao),
"domain_deposit".to_uppercase()=> u32_hex(spec.domain_deposit),
"domain_voluntary_exit".to_uppercase() => u32_hex(spec.domain_voluntary_exit),
"domain_selection_proof".to_uppercase() => u32_hex(spec.domain_selection_proof),
"domain_aggregate_and_proof".to_uppercase() => u32_hex(spec.domain_aggregate_and_proof),
"domain_application_mask".to_uppercase()=> u32_hex(spec.domain_application_mask),
"target_aggregators_per_committee".to_uppercase() =>
spec.target_aggregators_per_committee.to_string().into(),
"random_subnets_per_validator".to_uppercase() =>
spec.random_subnets_per_validator.to_string().into(),
"epochs_per_random_subnet_subscription".to_uppercase() =>
spec.epochs_per_random_subnet_subscription.to_string().into(),
"domain_contribution_and_proof".to_uppercase() =>
u32_hex(spec.domain_contribution_and_proof),
"domain_sync_committee".to_uppercase() => u32_hex(spec.domain_sync_committee),
"domain_sync_committee_selection_proof".to_uppercase() =>
u32_hex(spec.domain_sync_committee_selection_proof),
"sync_committee_subnet_count".to_uppercase() =>
altair::SYNC_COMMITTEE_SUBNET_COUNT.to_string().into(),
"target_aggregators_per_sync_subcommittee".to_uppercase() =>
altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE.to_string().into(),
}
}
@ -108,15 +112,16 @@ mod test {
.open(tmp_file.as_ref())
.expect("error opening file");
let mainnet_spec = ChainSpec::mainnet();
let mut yamlconfig = ConfigAndPreset::from_chain_spec::<MainnetEthSpec>(&mainnet_spec);
let mut yamlconfig =
ConfigAndPreset::from_chain_spec::<MainnetEthSpec>(&mainnet_spec, None);
let (k1, v1) = ("SAMPLE_HARDFORK_KEY1", "123456789");
let (k2, v2) = ("SAMPLE_HARDFORK_KEY2", "987654321");
let (k3, v3) = ("SAMPLE_HARDFORK_KEY3", 32);
let (k4, v4) = ("SAMPLE_HARDFORK_KEY4", Value::Null);
yamlconfig.extra_fields.insert(k1.into(), v1.into());
yamlconfig.extra_fields.insert(k2.into(), v2.into());
yamlconfig.extra_fields.insert(k3.into(), v3.into());
yamlconfig.extra_fields.insert(k4.into(), v4);
yamlconfig.extra_fields_mut().insert(k1.into(), v1.into());
yamlconfig.extra_fields_mut().insert(k2.into(), v2.into());
yamlconfig.extra_fields_mut().insert(k3.into(), v3.into());
yamlconfig.extra_fields_mut().insert(k4.into(), v4);
serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize");
@ -125,8 +130,8 @@ mod test {
.write(false)
.open(tmp_file.as_ref())
.expect("error while opening the file");
let from: ConfigAndPreset =
let from: ConfigAndPresetBellatrix =
serde_yaml::from_reader(reader).expect("error while deserializing");
assert_eq!(from, yamlconfig);
assert_eq!(ConfigAndPreset::Bellatrix(from), yamlconfig);
}
}

View File

@ -106,14 +106,14 @@ macro_rules! map_fork_name_with {
}
impl FromStr for ForkName {
type Err = ();
type Err = String;
fn from_str(fork_name: &str) -> Result<Self, ()> {
fn from_str(fork_name: &str) -> Result<Self, String> {
Ok(match fork_name.to_lowercase().as_ref() {
"phase0" | "base" => ForkName::Base,
"altair" => ForkName::Altair,
"bellatrix" | "merge" => ForkName::Merge,
_ => return Err(()),
_ => return Err(format!("unknown fork name: {}", fork_name)),
})
}
}
@ -138,7 +138,7 @@ impl TryFrom<String> for ForkName {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::from_str(&s).map_err(|()| format!("Invalid fork name: {}", s))
Self::from_str(&s)
}
}
@ -178,8 +178,8 @@ mod test {
assert_eq!(ForkName::from_str("AlTaIr"), Ok(ForkName::Altair));
assert_eq!(ForkName::from_str("altair"), Ok(ForkName::Altair));
assert_eq!(ForkName::from_str("NO_NAME"), Err(()));
assert_eq!(ForkName::from_str("no_name"), Err(()));
assert!(ForkName::from_str("NO_NAME").is_err());
assert!(ForkName::from_str("no_name").is_err());
}
#[test]

View File

@ -110,7 +110,9 @@ pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee};
pub use crate::beacon_state::{BeaconTreeHashCache, Error as BeaconStateError, *};
pub use crate::chain_spec::{ChainSpec, Config, Domain};
pub use crate::checkpoint::Checkpoint;
pub use crate::config_and_preset::ConfigAndPreset;
pub use crate::config_and_preset::{
ConfigAndPreset, ConfigAndPresetAltair, ConfigAndPresetBellatrix,
};
pub use crate::contribution_and_proof::ContributionAndProof;
pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH};
pub use crate::deposit_data::DepositData;

View File

@ -11,7 +11,7 @@ use std::process::Command;
use std::str::FromStr;
use std::string::ToString;
use tempfile::TempDir;
use types::{Address, Checkpoint, Epoch, ExecutionBlockHash, Hash256, MainnetEthSpec};
use types::{Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec};
use unused_port::{unused_tcp_port, unused_udp_port};
const DEFAULT_ETH1_ENDPOINT: &str = "http://localhost:8545/";
@ -949,6 +949,21 @@ fn http_tls_flags() {
});
}
#[test]
fn http_spec_fork_default() {
CommandLineTest::new()
.run_with_zero_port()
.with_config(|config| assert_eq!(config.http_api.spec_fork_name, None));
}
#[test]
fn http_spec_fork_override() {
CommandLineTest::new()
.flag("http-spec-fork", Some("altair"))
.run_with_zero_port()
.with_config(|config| assert_eq!(config.http_api.spec_fork_name, Some(ForkName::Altair)));
}
// Tests for Metrics flags.
#[test]
fn metrics_flag() {

View File

@ -7,7 +7,7 @@ use crate::http_metrics::metrics::{inc_counter_vec, ENDPOINT_ERRORS, ENDPOINT_RE
use environment::RuntimeContext;
use eth2::BeaconNodeHttpClient;
use futures::future;
use slog::{debug, error, info, warn, Logger};
use slog::{error, info, warn, Logger};
use slot_clock::SlotClock;
use std::fmt;
use std::fmt::Debug;
@ -16,7 +16,7 @@ use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
use tokio::{sync::RwLock, time::sleep};
use types::{ChainSpec, EthSpec};
use types::{ChainSpec, Config, EthSpec};
/// The number of seconds *prior* to slot start that we will try and update the state of fallback
/// nodes.
@ -213,9 +213,9 @@ impl<E: EthSpec> CandidateBeaconNode<E> {
/// Checks if the node has the correct specification.
async fn is_compatible(&self, spec: &ChainSpec, log: &Logger) -> Result<(), CandidateError> {
let config_and_preset = self
let config = self
.beacon_node
.get_config_spec()
.get_config_spec::<Config>()
.await
.map_err(|e| {
error!(
@ -228,25 +228,15 @@ impl<E: EthSpec> CandidateBeaconNode<E> {
})?
.data;
let beacon_node_spec =
ChainSpec::from_config::<E>(&config_and_preset.config).ok_or_else(|| {
error!(
log,
"The minimal/mainnet spec type of the beacon node does not match the validator \
client. See the --network command.";
"endpoint" => %self.beacon_node,
);
CandidateError::Incompatible
})?;
if !config_and_preset.extra_fields.is_empty() {
debug!(
let beacon_node_spec = ChainSpec::from_config::<E>(&config).ok_or_else(|| {
error!(
log,
"Beacon spec includes unknown fields";
"The minimal/mainnet spec type of the beacon node does not match the validator \
client. See the --network command.";
"endpoint" => %self.beacon_node,
"fields" => ?config_and_preset.extra_fields,
);
}
CandidateError::Incompatible
})?;
if beacon_node_spec.genesis_fork_version != spec.genesis_fork_version {
error!(

View File

@ -217,8 +217,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.and(signer.clone())
.and_then(|spec: Arc<_>, signer| {
blocking_signed_json_task(signer, move || {
let mut config = ConfigAndPreset::from_chain_spec::<E>(&spec);
config.make_backwards_compat(&spec);
let config = ConfigAndPreset::from_chain_spec::<E>(&spec, None);
Ok(api_types::GenericResponse::from(config))
})
});

View File

@ -208,10 +208,13 @@ impl ApiTester {
}
pub async fn test_get_lighthouse_spec(self) -> Self {
let result = self.client.get_lighthouse_spec().await.unwrap().data;
let mut expected = ConfigAndPreset::from_chain_spec::<E>(&E::default_spec());
expected.make_backwards_compat(&E::default_spec());
let result = self
.client
.get_lighthouse_spec::<ConfigAndPresetBellatrix>()
.await
.map(|res| ConfigAndPreset::Bellatrix(res.data))
.unwrap();
let expected = ConfigAndPreset::from_chain_spec::<E>(&E::default_spec(), None);
assert_eq!(result, expected);
@ -623,7 +626,9 @@ fn routes_with_invalid_auth() {
.await
.test_with_invalid_auth(|client| async move { client.get_lighthouse_health().await })
.await
.test_with_invalid_auth(|client| async move { client.get_lighthouse_spec().await })
.test_with_invalid_auth(|client| async move {
client.get_lighthouse_spec::<types::Config>().await
})
.await
.test_with_invalid_auth(
|client| async move { client.get_lighthouse_validators().await },