From 86f4c7c88efc90bc7181a11ae8f880da7f101650 Mon Sep 17 00:00:00 2001 From: "emlautarom1-agent[bot]" <292495798+emlautarom1-agent[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:03:09 -0300 Subject: [PATCH 1/4] feat(app): auto-enable GnosisBlockHotfix for gnosis/chiado from the lock fork version Derive the network from the cluster lock's genesis fork version and force-enable GnosisBlockHotfix for gnosis/chiado unless the operator explicitly disabled it (Charon app/app.go:151/197-203: featureset.Init + ForkVersionToNetwork + EnableGnosisBlockHotfixIfNotDisabled). Without this, gnosis/chiado clusters would sign proposals over the wrong block hash. AppConfig now carries pluto_featureset::Config instead of a resolved Arc, since the auto-enable needs &mut FeatureSet plus the Config and the network is only known after lock load. The FeatureSet is resolved inside App::run via the new resolve_feature_set helper (which also validates the minimum status, the analog of featureset.Init) and then wrapped in Arc. The CLI bridge (build_feature_config) parses the flags and forwards the Config through. Closes #530 --- crates/app/Cargo.toml | 2 +- crates/app/src/node/config.rs | 11 ++-- crates/app/src/node/mod.rs | 103 ++++++++++++++++++++++++++++++++- crates/app/tests/simnet.rs | 4 +- crates/cli/src/commands/run.rs | 52 ++++++++++------- 5 files changed, 140 insertions(+), 32 deletions(-) diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 3214fce6..1cd17e9e 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -18,6 +18,7 @@ pluto-consensus.workspace = true pluto-eth1wrap.workspace = true pluto-featureset.workspace = true pluto-eth2api.workspace = true +pluto-eth2util.workspace = true pluto-p2p.workspace = true pluto-parsigex.workspace = true pluto-peerinfo.workspace = true @@ -50,7 +51,6 @@ pluto-ssz.workspace = true # `--simnet-validator-mock`), mirroring Charon which always compiles its mocks # into the binary. pluto-testutil.workspace = true -pluto-eth2util.workspace = true [build-dependencies] pluto-build-proto.workspace = true diff --git a/crates/app/src/node/config.rs b/crates/app/src/node/config.rs index bae915cb..9c6c6c0d 100644 --- a/crates/app/src/node/config.rs +++ b/crates/app/src/node/config.rs @@ -1,8 +1,7 @@ //! Configuration for the distributed-validator node. -use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; +use std::{net::SocketAddr, path::PathBuf, time::Duration}; -use pluto_featureset::FeatureSet; use pluto_p2p::config::P2PConfig; /// Application configuration for running a distributed-validator node. @@ -69,10 +68,10 @@ pub struct AppConfig { /// Disable appending the client version/codex to graffiti. pub graffiti_disable_client_append: bool, - /// Feature set controlling optional/alpha behaviors (e.g. - /// `FetchOnlyCommIdx0`, `ChainSplitHalt`). Resolved from the CLI - /// feature flags (out of scope here). - pub feature_set: Arc, + /// Feature set configuration controlling optional/alpha (e.g. + /// `FetchOnlyCommIdx0`, `ChainSplitHalt`) and chain specific behaviors + /// (ex. `GnosisBlockHotfix`) that gets resolved into a `FeatureSet`. + pub feature: pluto_featureset::Config, /// Enable the in-process simnet mock beacon node. When set, the beacon /// clients target an internal `BeaconMock` seeded with the cluster's diff --git a/crates/app/src/node/mod.rs b/crates/app/src/node/mod.rs index 3bdb29ce..88f12169 100644 --- a/crates/app/src/node/mod.rs +++ b/crates/app/src/node/mod.rs @@ -193,6 +193,10 @@ pub enum AppError { /// Simnet (beacon/validator mock) setup failed. #[error("simnet: {0}")] Simnet(String), + + /// Feature set resolution failed (invalid minimum status). + #[error("feature set: {0}")] + FeatureSet(#[from] pluto_featureset::FeaturesetError), } /// A wired, runnable distributed-validator node. @@ -233,6 +237,10 @@ async fn run(config: AppConfig, ct: CancellationToken) -> Result<(), AppError> { pluto_cluster::load::load_cluster_lock(&config.lock_file, config.no_verify, ð1).await?; let threshold = lock.threshold; + // Resolve the injectable feature set now that the lock's fork version is + // known. + let feature_set = Arc::new(resolve_feature_set(&config.feature, &lock.fork_version)?); + let key = pluto_k1util::load(&config.priv_key_file)?; // Guards against a second node running against the same key. @@ -369,8 +377,6 @@ async fn run(config: AppConfig, ct: CancellationToken) -> Result<(), AppError> { .unwrap_or(0) .saturating_mul(slots_per_epoch); - // Feature set drives optional/alpha behaviors. - let feature_set = Arc::clone(&config.feature_set); let fetch_only_comm_idx0 = feature_set.enabled(pluto_featureset::Feature::FetchOnlyCommIdx0); // ---- Consensus (built directly; shared with p2p behaviour + core stitch) ---- @@ -952,6 +958,27 @@ fn network_name_or_hex(fork_version: &[u8]) -> String { .unwrap_or_else(|_| format!("0x{}", hex::encode(fork_version))) } +/// Resolves the injectable feature set from its config and the cluster's fork +/// version. +/// +/// Performs the Rust analogs of Charon's `featureset.Init` and, for +/// gnosis/chiado, `EnableGnosisBlockHotfixIfNotDisabled`. +fn resolve_feature_set( + config: &pluto_featureset::Config, + fork_version: &[u8], +) -> Result { + let mut feature_set = pluto_featureset::FeatureSet::from_config(config.clone())?; + + if let Ok(network) = pluto_eth2util::network::fork_version_to_network(fork_version) + && (network == pluto_eth2util::network::GNOSIS.name + || network == pluto_eth2util::network::CHIADO.name) + { + feature_set.enable_gnosis_block_hotfix_if_not_disabled(config); + } + + Ok(feature_set) +} + /// Builds an [`EthBeaconNodeApiClient`](pluto_eth2api::EthBeaconNodeApiClient) /// for `base_url` with the given request timeout. fn build_api_client( @@ -1332,4 +1359,76 @@ mod tests { other => panic!("expected ForkScheduleMismatch, got {other:?}"), } } + + /// Decodes a predefined network's genesis fork version into raw bytes. + fn fork_version_bytes(hex_str: &str) -> Vec { + hex::decode(hex_str.strip_prefix("0x").unwrap_or(hex_str)).expect("valid fork version hex") + } + + #[test] + fn resolve_feature_set_auto_enables_gnosis_hotfix_for_gnosis_and_chiado() { + use pluto_eth2util::network::{CHIADO, GNOSIS}; + use pluto_featureset::{Config, Feature}; + + // Default config leaves GnosisBlockHotfix at alpha (off under the stable + // minimum), so the auto-enable is what flips it on. + for network in [GNOSIS, CHIADO] { + let feature_set = resolve_feature_set( + &Config::default(), + &fork_version_bytes(network.genesis_fork_version_hex), + ) + .expect("resolve should succeed"); + assert!( + feature_set.enabled(Feature::GnosisBlockHotfix), + "GnosisBlockHotfix should be auto-enabled for {}", + network.name + ); + } + } + + #[test] + fn resolve_feature_set_leaves_other_networks_untouched() { + use pluto_eth2util::network::MAINNET; + use pluto_featureset::{Config, Feature}; + + let feature_set = resolve_feature_set( + &Config::default(), + &fork_version_bytes(MAINNET.genesis_fork_version_hex), + ) + .expect("resolve should succeed"); + + assert!(!feature_set.enabled(Feature::GnosisBlockHotfix)); + } + + #[test] + fn resolve_feature_set_respects_explicit_gnosis_hotfix_disable() { + use pluto_eth2util::network::GNOSIS; + use pluto_featureset::{Config, Feature, Status}; + + // Explicitly disabling the feature must win even on gnosis, matching + // Charon's `EnableGnosisBlockHotfixIfNotDisabled`. + let config = Config { + min_status: Status::Stable, + enabled: vec![], + disabled: vec![Feature::GnosisBlockHotfix], + }; + let feature_set = resolve_feature_set( + &config, + &fork_version_bytes(GNOSIS.genesis_fork_version_hex), + ) + .expect("resolve should succeed"); + + assert!(!feature_set.enabled(Feature::GnosisBlockHotfix)); + } + + #[test] + fn resolve_feature_set_skips_unrecognized_fork_version() { + use pluto_featureset::{Config, Feature}; + + // An unknown fork version must not panic and must not auto-enable. + let feature_set = resolve_feature_set(&Config::default(), &[0x01, 0x02, 0x03, 0x04]) + .expect("resolve should succeed"); + + assert!(!feature_set.enabled(Feature::GnosisBlockHotfix)); + } } diff --git a/crates/app/tests/simnet.rs b/crates/app/tests/simnet.rs index ee8219b8..ea9856ae 100644 --- a/crates/app/tests/simnet.rs +++ b/crates/app/tests/simnet.rs @@ -11,7 +11,7 @@ //! duty-submission path is covered by //! `multinode_parsig_exchange_reaches_submission` in `tests/wiring.rs`. -use std::{net::SocketAddr, sync::Arc, time::Duration}; +use std::{net::SocketAddr, time::Duration}; use pluto_app::node::{App, AppConfig}; use tokio_util::sync::CancellationToken; @@ -76,7 +76,7 @@ async fn simnet_single_node_boots_and_serves_validator_api() { eth1_endpoint: None, graffiti: None, graffiti_disable_client_append: false, - feature_set: Arc::new(pluto_featureset::FeatureSet::default()), + feature: pluto_featureset::Config::default(), simnet_beacon_mock: true, simnet_validator_mock: true, simnet_beacon_mock_fuzz: false, diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index 1e700262..af64ea0d 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -29,13 +29,12 @@ use std::{ collections::HashMap, net::{SocketAddr, ToSocketAddrs}, path::{Path, PathBuf}, - sync::Arc, time::Duration as StdDuration, }; use libp2p::multiaddr::Protocol; use pluto_eth2util::helpers::validate_http_headers; -use pluto_featureset::{Feature, FeatureSet, FeaturesetError, Status}; +use pluto_featureset::{Feature, FeaturesetError, Status}; use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; @@ -999,7 +998,7 @@ fn build_app_config(config: RunConfig) -> Result { check_unsupported_flags(&config)?; warn_ignored_flags(&config); - let feature_set = build_feature_set(&config.feature)?; + let feature_config = build_feature_config(&config.feature)?; let validator_api_addr = parse_socket_addr("validator-api-address", &config.validator_api_addr)?; let monitoring_addr = parse_socket_addr("monitoring-address", &config.monitoring_addr)?; @@ -1077,7 +1076,7 @@ fn build_app_config(config: RunConfig) -> Result { // validator" (`NewGraffitiBuilder`), matching `AppConfig`'s `None`. graffiti: (!graffiti.is_empty()).then_some(graffiti), graffiti_disable_client_append, - feature_set, + feature: feature_config, simnet_beacon_mock, simnet_validator_mock, simnet_beacon_mock_fuzz, @@ -1152,14 +1151,16 @@ fn warn_ignored_flags(config: &RunConfig) { } } -/// Resolves the feature-set flags into an injectable [`FeatureSet`]. +/// Parses the feature-set flags into a [`pluto_featureset::Config`]. /// -/// Matches Charon: an unknown min status is a hard error, while unknown -/// enabled/disabled feature names are warned about and ignored. -fn build_feature_set(feature: &FeatureConfig) -> Result> { - // `Status::try_from` also parses statuses like "enable" that are not valid - // *min* statuses; `FeatureSet::from_config` rejects those below, so both - // paths surface Charon's "unknown min status" error. +/// The config is forwarded to the app and resolved into an injectable +/// `FeatureSet` inside `App::run` (after lock load, so the gnosis/chiado +/// `GnosisBlockHotfix` auto-enable can key off the fork version). Matches +/// Charon's `featureset.Init`: an unknown min status string is a hard error +/// here, while unknown enabled/disabled feature names are warned about and +/// ignored. Statuses that parse but are not valid *minimum* statuses (e.g. +/// `enable`) are rejected later by `FeatureSet::from_config`. +fn build_feature_config(feature: &FeatureConfig) -> Result { let min_status = Status::try_from(feature.min_status.as_str()).map_err(|_| { FeaturesetError::UnknownMinStatus { min_status: feature.min_status.clone(), @@ -1177,13 +1178,11 @@ fn build_feature_set(feature: &FeatureConfig) -> Result> { .collect() }; - let feature_set = FeatureSet::from_config(pluto_featureset::Config { + Ok(pluto_featureset::Config { min_status, enabled: parse_features(&feature.enabled, "enabled"), disabled: parse_features(&feature.disabled, "disabled"), - })?; - - Ok(Arc::new(feature_set)) + }) } /// Parses an HTTP listen address flag (`flag` names it for errors). Unlike a @@ -1757,6 +1756,13 @@ mod tests { .to_string() } + /// Resolves the forwarded feature config into a `FeatureSet`, as `App::run` + /// does (the gnosis/chiado fork-version step is covered in the app crate). + fn resolved_feature_set(config: &pluto_app::node::AppConfig) -> pluto_featureset::FeatureSet { + pluto_featureset::FeatureSet::from_config(config.feature.clone()) + .expect("feature config should resolve") + } + #[test] fn build_app_config_maps_fields() { let config = app_config(&[ @@ -1814,8 +1820,9 @@ mod tests { "127.0.0.1:3600".parse::().expect("socket addr") ); // The default feature set enables stable features only. - assert!(config.feature_set.enabled(Feature::EagerDoubleLinear)); - assert!(!config.feature_set.enabled(Feature::MockAlpha)); + let feature_set = resolved_feature_set(&config); + assert!(feature_set.enabled(Feature::EagerDoubleLinear)); + assert!(!feature_set.enabled(Feature::MockAlpha)); } #[test] @@ -2042,18 +2049,21 @@ mod tests { } #[test] - fn build_app_config_resolves_feature_set() { + fn build_app_config_forwards_feature_config() { + // The bridge forwards a `featureset::Config`; `App::run` resolves it. + // Assert on the resolved set to keep the semantics observable here. + // Min status is parsed case-insensitively, like Charon. let config = app_config(&["--feature-set=ALPHA"]).expect("alpha min status"); - assert!(config.feature_set.enabled(Feature::MockAlpha)); + assert!(resolved_feature_set(&config).enabled(Feature::MockAlpha)); let config = app_config(&["--feature-set-enable=chain_split_halt"]).expect("explicit enable"); - assert!(config.feature_set.enabled(Feature::ChainSplitHalt)); + assert!(resolved_feature_set(&config).enabled(Feature::ChainSplitHalt)); let config = app_config(&["--feature-set-disable=eager_double_linear"]).expect("explicit disable"); - assert!(!config.feature_set.enabled(Feature::EagerDoubleLinear)); + assert!(!resolved_feature_set(&config).enabled(Feature::EagerDoubleLinear)); // Unknown feature names are warned about and ignored, like Charon. app_config(&["--feature-set-enable=not_a_feature"]).expect("unknown feature ignored"); From 3d7f9c6935677654d663d85bd6d389dddf8fb546 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Thu, 16 Jul 2026 13:50:45 -0300 Subject: [PATCH 2/4] Remove irrelevant context --- crates/cli/src/commands/run.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index af64ea0d..91831585 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -1152,14 +1152,6 @@ fn warn_ignored_flags(config: &RunConfig) { } /// Parses the feature-set flags into a [`pluto_featureset::Config`]. -/// -/// The config is forwarded to the app and resolved into an injectable -/// `FeatureSet` inside `App::run` (after lock load, so the gnosis/chiado -/// `GnosisBlockHotfix` auto-enable can key off the fork version). Matches -/// Charon's `featureset.Init`: an unknown min status string is a hard error -/// here, while unknown enabled/disabled feature names are warned about and -/// ignored. Statuses that parse but are not valid *minimum* statuses (e.g. -/// `enable`) are rejected later by `FeatureSet::from_config`. fn build_feature_config(feature: &FeatureConfig) -> Result { let min_status = Status::try_from(feature.min_status.as_str()).map_err(|_| { FeaturesetError::UnknownMinStatus { From 26637f5f28c7e13de453e4a90695e04639ba9d93 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 21 Jul 2026 08:18:55 -0300 Subject: [PATCH 3/4] fix(run): rename feature config function for clarity --- crates/cli/src/commands/run.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index 91831585..bccc3a77 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -998,7 +998,7 @@ fn build_app_config(config: RunConfig) -> Result { check_unsupported_flags(&config)?; warn_ignored_flags(&config); - let feature_config = build_feature_config(&config.feature)?; + let feature_config = parse_featureset_config(&config.feature)?; let validator_api_addr = parse_socket_addr("validator-api-address", &config.validator_api_addr)?; let monitoring_addr = parse_socket_addr("monitoring-address", &config.monitoring_addr)?; @@ -1152,7 +1152,7 @@ fn warn_ignored_flags(config: &RunConfig) { } /// Parses the feature-set flags into a [`pluto_featureset::Config`]. -fn build_feature_config(feature: &FeatureConfig) -> Result { +fn parse_featureset_config(feature: &FeatureConfig) -> Result { let min_status = Status::try_from(feature.min_status.as_str()).map_err(|_| { FeaturesetError::UnknownMinStatus { min_status: feature.min_status.clone(), From 3a9f7828165f2c6384fd195a9a8f600f45b3da49 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Thu, 23 Jul 2026 19:37:50 -0300 Subject: [PATCH 4/4] Improve `FeatureSet` naming --- crates/app/src/node/config.rs | 8 ++++---- crates/app/src/node/mod.rs | 5 ++++- crates/app/tests/simnet.rs | 2 +- crates/cli/src/commands/run.rs | 20 ++++++++++---------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/app/src/node/config.rs b/crates/app/src/node/config.rs index 9c6c6c0d..5167045d 100644 --- a/crates/app/src/node/config.rs +++ b/crates/app/src/node/config.rs @@ -68,10 +68,10 @@ pub struct AppConfig { /// Disable appending the client version/codex to graffiti. pub graffiti_disable_client_append: bool, - /// Feature set configuration controlling optional/alpha (e.g. - /// `FetchOnlyCommIdx0`, `ChainSplitHalt`) and chain specific behaviors - /// (ex. `GnosisBlockHotfix`) that gets resolved into a `FeatureSet`. - pub feature: pluto_featureset::Config, + /// Feature-set configuration for optional/alpha capabilities (e.g. + /// `FetchOnlyCommIdx0`, `ChainSplitHalt`) and chain-specific behavior + /// (e.g. `GnosisBlockHotfix`), resolved into a `FeatureSet`. + pub feature_set: pluto_featureset::Config, /// Enable the in-process simnet mock beacon node. When set, the beacon /// clients target an internal `BeaconMock` seeded with the cluster's diff --git a/crates/app/src/node/mod.rs b/crates/app/src/node/mod.rs index 88f12169..d7a766ec 100644 --- a/crates/app/src/node/mod.rs +++ b/crates/app/src/node/mod.rs @@ -239,7 +239,10 @@ async fn run(config: AppConfig, ct: CancellationToken) -> Result<(), AppError> { // Resolve the injectable feature set now that the lock's fork version is // known. - let feature_set = Arc::new(resolve_feature_set(&config.feature, &lock.fork_version)?); + let feature_set = Arc::new(resolve_feature_set( + &config.feature_set, + &lock.fork_version, + )?); let key = pluto_k1util::load(&config.priv_key_file)?; diff --git a/crates/app/tests/simnet.rs b/crates/app/tests/simnet.rs index ea9856ae..6ac087f7 100644 --- a/crates/app/tests/simnet.rs +++ b/crates/app/tests/simnet.rs @@ -76,7 +76,7 @@ async fn simnet_single_node_boots_and_serves_validator_api() { eth1_endpoint: None, graffiti: None, graffiti_disable_client_append: false, - feature: pluto_featureset::Config::default(), + feature_set: pluto_featureset::Config::default(), simnet_beacon_mock: true, simnet_validator_mock: true, simnet_beacon_mock_fuzz: false, diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index bccc3a77..8db86143 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -605,7 +605,7 @@ impl From<&TestnetConfig> for pluto_eth2util::network::Network { /// Feature set configuration. #[derive(Debug, Clone, Default)] -pub struct FeatureConfig { +pub struct FeatureSetConfig { /// Minimum feature status to enable by default (alpha/beta/stable). pub min_status: String, /// Features to enable on top of the minimum set. @@ -625,7 +625,7 @@ pub struct RunConfig { /// Tracing configuration built from [`RunLogArgs`]/[`RunLokiArgs`]. pub log: pluto_tracing::TracingConfig, /// Feature set configuration. - pub feature: FeatureConfig, + pub feature_set: FeatureSetConfig, /// Path to the cluster lock file. pub lock_file: String, /// Path to the cluster manifest file. @@ -814,7 +814,7 @@ impl TryFrom for RunConfig { Ok(Self { p2p: p2p_config, log: log_config, - feature: FeatureConfig { + feature_set: FeatureSetConfig { min_status: feature.feature_set, enabled: feature.feature_set_enable, disabled: feature.feature_set_disable, @@ -998,7 +998,7 @@ fn build_app_config(config: RunConfig) -> Result { check_unsupported_flags(&config)?; warn_ignored_flags(&config); - let feature_config = parse_featureset_config(&config.feature)?; + let feature_config = parse_featureset_config(&config.feature_set)?; let validator_api_addr = parse_socket_addr("validator-api-address", &config.validator_api_addr)?; let monitoring_addr = parse_socket_addr("monitoring-address", &config.monitoring_addr)?; @@ -1016,7 +1016,7 @@ fn build_app_config(config: RunConfig) -> Result { let RunConfig { p2p, log: _, - feature: _, + feature_set: _, lock_file, manifest_file: _, no_verify, @@ -1076,7 +1076,7 @@ fn build_app_config(config: RunConfig) -> Result { // validator" (`NewGraffitiBuilder`), matching `AppConfig`'s `None`. graffiti: (!graffiti.is_empty()).then_some(graffiti), graffiti_disable_client_append, - feature: feature_config, + feature_set: feature_config, simnet_beacon_mock, simnet_validator_mock, simnet_beacon_mock_fuzz, @@ -1152,7 +1152,7 @@ fn warn_ignored_flags(config: &RunConfig) { } /// Parses the feature-set flags into a [`pluto_featureset::Config`]. -fn parse_featureset_config(feature: &FeatureConfig) -> Result { +fn parse_featureset_config(feature: &FeatureSetConfig) -> Result { let min_status = Status::try_from(feature.min_status.as_str()).map_err(|_| { FeaturesetError::UnknownMinStatus { min_status: feature.min_status.clone(), @@ -1723,9 +1723,9 @@ mod tests { assert_eq!(config.debug_addr, "127.0.0.1:9630"); assert_eq!(config.p2p.relays.len(), 2); assert_eq!(config.p2p.tcp_addrs, vec!["0.0.0.0:9000".to_string()]); - assert_eq!(config.feature.min_status, "alpha"); + assert_eq!(config.feature_set.min_status, "alpha"); assert_eq!( - config.feature.enabled, + config.feature_set.enabled, vec!["feat_a".to_string(), "feat_b".to_string()] ); // p2p_fuzz is never set on the safe `run` path. @@ -1751,7 +1751,7 @@ mod tests { /// Resolves the forwarded feature config into a `FeatureSet`, as `App::run` /// does (the gnosis/chiado fork-version step is covered in the app crate). fn resolved_feature_set(config: &pluto_app::node::AppConfig) -> pluto_featureset::FeatureSet { - pluto_featureset::FeatureSet::from_config(config.feature.clone()) + pluto_featureset::FeatureSet::from_config(config.feature_set.clone()) .expect("feature config should resolve") }