Validator manager import to allow overriding fields with CLI flag (#7684)

* #7651


  


Co-Authored-By: Tan Chee Keong <tanck@sigmaprime.io>

Co-Authored-By: chonghe <44791194+chong-he@users.noreply.github.com>

Co-Authored-By: Lion - dapplion <35266934+dapplion@users.noreply.github.com>
This commit is contained in:
chonghe
2026-02-19 05:55:16 +08:00
committed by GitHub
parent fab77f4fc9
commit 5e2d296de6
3 changed files with 183 additions and 46 deletions

View File

@@ -24,6 +24,9 @@ Options:
--debug-level <LEVEL>
Specifies the verbosity level used when emitting logs to the terminal.
[default: info] [possible values: info, debug, trace, warn, error]
--enabled <enabled>
When provided, the imported validator will be enabled or disabled.
[possible values: true, false]
--gas-limit <UINT64>
When provided, the imported validator will use this gas limit. It is
recommended to leave this as the default value by not specifying this

View File

@@ -16,6 +16,7 @@ use validator_manager::{
list_validators::ListConfig,
move_validators::{MoveConfig, PasswordSource, Validators},
};
use zeroize::Zeroizing;
const EXAMPLE_ETH1_ADDRESS: &str = "0x00000000219ab540356cBB839Cbe05303d7705Fa";
@@ -280,6 +281,40 @@ pub fn validator_import_using_both_file_flags() {
.assert_failed();
}
#[test]
pub fn validator_import_keystore_file_without_password_flag_should_fail() {
CommandLineTest::validators_import()
.flag("--vc-token", Some("./token.json"))
.flag("--keystore-file", Some("./keystore.json"))
.assert_failed();
}
#[test]
pub fn validator_import_keystore_file_with_password_flag_should_pass() {
CommandLineTest::validators_import()
.flag("--vc-token", Some("./token.json"))
.flag("--keystore-file", Some("./keystore.json"))
.flag("--password", Some("abcd"))
.assert_success(|config| {
let expected = ImportConfig {
validators_file_path: None,
keystore_file_path: Some(PathBuf::from("./keystore.json")),
vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(),
vc_token_path: PathBuf::from("./token.json"),
ignore_duplicates: false,
password: Some(Zeroizing::new("abcd".into())),
fee_recipient: None,
builder_boost_factor: None,
gas_limit: None,
builder_proposals: None,
enabled: None,
prefer_builder_proposals: None,
};
assert_eq!(expected, config);
println!("{:?}", expected);
});
}
#[test]
pub fn validator_import_missing_both_file_flags() {
CommandLineTest::validators_import()
@@ -287,6 +322,36 @@ pub fn validator_import_missing_both_file_flags() {
.assert_failed();
}
#[test]
pub fn validator_import_fee_recipient_override() {
CommandLineTest::validators_import()
.flag("--validators-file", Some("./vals.json"))
.flag("--vc-token", Some("./token.json"))
.flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS))
.flag("--gas-limit", Some("1337"))
.flag("--builder-proposals", Some("true"))
.flag("--builder-boost-factor", Some("150"))
.flag("--prefer-builder-proposals", Some("true"))
.flag("--enabled", Some("false"))
.assert_success(|config| {
let expected = ImportConfig {
validators_file_path: Some(PathBuf::from("./vals.json")),
keystore_file_path: None,
vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(),
vc_token_path: PathBuf::from("./token.json"),
ignore_duplicates: false,
password: None,
fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()),
builder_boost_factor: Some(150),
gas_limit: Some(1337),
builder_proposals: Some(true),
enabled: Some(false),
prefer_builder_proposals: Some(true),
};
assert_eq!(expected, config);
});
}
#[test]
pub fn validator_move_defaults() {
CommandLineTest::validators_move()

View File

@@ -112,8 +112,7 @@ pub fn cli_app() -> Command {
.value_name("ETH1_ADDRESS")
.help("When provided, the imported validator will use the suggested fee recipient. Omit this flag to use the default value from the VC.")
.action(ArgAction::Set)
.display_order(0)
.requires(KEYSTORE_FILE_FLAG),
.display_order(0),
)
.arg(
Arg::new(GAS_LIMIT)
@@ -122,8 +121,7 @@ pub fn cli_app() -> Command {
.help("When provided, the imported validator will use this gas limit. It is recommended \
to leave this as the default value by not specifying this flag.",)
.action(ArgAction::Set)
.display_order(0)
.requires(KEYSTORE_FILE_FLAG),
.display_order(0),
)
.arg(
Arg::new(BUILDER_PROPOSALS)
@@ -132,8 +130,7 @@ pub fn cli_app() -> Command {
blocks via builder rather than the local EL.",)
.value_parser(["true","false"])
.action(ArgAction::Set)
.display_order(0)
.requires(KEYSTORE_FILE_FLAG),
.display_order(0),
)
.arg(
Arg::new(BUILDER_BOOST_FACTOR)
@@ -144,8 +141,7 @@ pub fn cli_app() -> Command {
when choosing between a builder payload header and payload from \
the local execution node.",)
.action(ArgAction::Set)
.display_order(0)
.requires(KEYSTORE_FILE_FLAG),
.display_order(0),
)
.arg(
Arg::new(PREFER_BUILDER_PROPOSALS)
@@ -154,8 +150,16 @@ pub fn cli_app() -> Command {
constructed by builders, regardless of payload value.",)
.value_parser(["true","false"])
.action(ArgAction::Set)
.display_order(0)
.requires(KEYSTORE_FILE_FLAG),
.display_order(0),
)
.arg(
Arg::new(ENABLED)
.long(ENABLED)
.help("When provided, the imported validator will be \
enabled or disabled.",)
.value_parser(["true","false"])
.action(ArgAction::Set)
.display_order(0),
)
}
@@ -225,48 +229,113 @@ async fn run(config: ImportConfig) -> Result<(), String> {
enabled,
} = config;
let validators: Vec<ValidatorSpecification> =
if let Some(validators_format_path) = &validators_file_path {
if !validators_format_path.exists() {
return Err(format!(
"Unable to find file at {:?}",
validators_format_path
));
}
let validators: Vec<ValidatorSpecification> = if let Some(validators_format_path) =
&validators_file_path
{
if !validators_format_path.exists() {
return Err(format!(
"Unable to find file at {:?}",
validators_format_path
));
}
let validators_file = fs::OpenOptions::new()
.read(true)
.create(false)
.open(validators_format_path)
.map_err(|e| format!("Unable to open {:?}: {:?}", validators_format_path, e))?;
let validators_file = fs::OpenOptions::new()
.read(true)
.create(false)
.open(validators_format_path)
.map_err(|e| format!("Unable to open {:?}: {:?}", validators_format_path, e))?;
serde_json::from_reader(&validators_file).map_err(|e| {
// Define validators as mutable so that if a relevant flag is supplied, the fields can be overridden.
let mut validators: Vec<ValidatorSpecification> = serde_json::from_reader(&validators_file)
.map_err(|e| {
format!(
"Unable to parse JSON in {:?}: {:?}",
validators_format_path, e
)
})?
} else if let Some(keystore_format_path) = &keystore_file_path {
vec![ValidatorSpecification {
voting_keystore: KeystoreJsonStr(
Keystore::from_json_file(keystore_format_path).map_err(|e| format!("{e:?}"))?,
),
voting_keystore_password: password.ok_or_else(|| {
"The --password flag is required to supply the keystore password".to_string()
})?,
slashing_protection: None,
fee_recipient,
gas_limit,
builder_proposals,
builder_boost_factor,
prefer_builder_proposals,
enabled,
}]
} else {
return Err(format!(
"One of the flag --{VALIDATORS_FILE_FLAG} or --{KEYSTORE_FILE_FLAG} is required."
));
};
})?;
// Log the overridden note when one or more flags is supplied
if let Some(override_fee_recipient) = fee_recipient {
eprintln!(
"Please note! --suggested-fee-recipient is provided. This will override existing fee recipient defined in validators.json with: {:?}",
override_fee_recipient
);
}
if let Some(override_gas_limit) = gas_limit {
eprintln!(
"Please note! --gas-limit is provided. This will override existing gas limit defined in validators.json with: {}",
override_gas_limit
);
}
if let Some(override_builder_proposals) = builder_proposals {
eprintln!(
"Please note! --builder-proposals is provided. This will override existing builder proposal setting defined in validators.json with: {}",
override_builder_proposals
);
}
if let Some(override_builder_boost_factor) = builder_boost_factor {
eprintln!(
"Please note! --builder-boost-factor is provided. This will override existing builder boost factor defined in validators.json with: {}",
override_builder_boost_factor
);
}
if let Some(override_prefer_builder_proposals) = prefer_builder_proposals {
eprintln!(
"Please note! --prefer-builder-proposals is provided. This will override existing prefer builder proposal setting defined in validators.json with: {}",
override_prefer_builder_proposals
);
}
if let Some(override_enabled) = enabled {
eprintln!(
"Please note! --enabled flag is provided. This will override existing setting defined in validators.json with: {}",
override_enabled
);
}
// Override the fields in validators.json file if the flag is supplied
for validator in &mut validators {
if let Some(override_fee_recipient) = fee_recipient {
validator.fee_recipient = Some(override_fee_recipient);
}
if let Some(override_gas_limit) = gas_limit {
validator.gas_limit = Some(override_gas_limit);
}
if let Some(override_builder_proposals) = builder_proposals {
validator.builder_proposals = Some(override_builder_proposals);
}
if let Some(override_builder_boost_factor) = builder_boost_factor {
validator.builder_boost_factor = Some(override_builder_boost_factor);
}
if let Some(override_prefer_builder_proposals) = prefer_builder_proposals {
validator.prefer_builder_proposals = Some(override_prefer_builder_proposals);
}
if let Some(override_enabled) = enabled {
validator.enabled = Some(override_enabled);
}
}
validators
} else if let Some(keystore_format_path) = &keystore_file_path {
vec![ValidatorSpecification {
voting_keystore: KeystoreJsonStr(
Keystore::from_json_file(keystore_format_path).map_err(|e| format!("{e:?}"))?,
),
voting_keystore_password: password.ok_or_else(|| {
"The --password flag is required to supply the keystore password".to_string()
})?,
slashing_protection: None,
fee_recipient,
gas_limit,
builder_proposals,
builder_boost_factor,
prefer_builder_proposals,
enabled,
}]
} else {
return Err(format!(
"One of the flag --{VALIDATORS_FILE_FLAG} or --{KEYSTORE_FILE_FLAG} is required."
));
};
let count = validators.len();