mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-07 00:42:42 +00:00
added check for fee recipient per validator and added unit tests (#8454)
Addresses #5403 - Added `check_fee_recipient()` method to validate individual validators - Added `check_all_fee_recipients()` to validate all validators on startup - Validator client now fails to start if any enabled validator lacks a fee recipient and no global flag is used. - Added Clear error messages to guide users on how to fix the issue - Added unit tests Co-Authored-By: AbolareRoheemah <roheemahabo@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ use std::collections::HashSet;
|
|||||||
use std::fs::{self, File, create_dir_all};
|
use std::fs::{self, File, create_dir_all};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tracing::error;
|
use tracing::{debug, error};
|
||||||
use types::{Address, graffiti::GraffitiString};
|
use types::{Address, graffiti::GraffitiString};
|
||||||
use validator_dir::VOTING_KEYSTORE_FILE;
|
use validator_dir::VOTING_KEYSTORE_FILE;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
@@ -212,6 +212,16 @@ impl ValidatorDefinition {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn check_fee_recipient(&self, global_fee_recipient: Option<Address>) -> Option<&PublicKey> {
|
||||||
|
// Skip disabled validators. Also skip if validator has its own fee set, or the global flag is set
|
||||||
|
if !self.enabled || self.suggested_fee_recipient.is_some() || global_fee_recipient.is_some()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(&self.voting_public_key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A list of `ValidatorDefinition` that serves as a serde-able configuration file which defines a
|
/// A list of `ValidatorDefinition` that serves as a serde-able configuration file which defines a
|
||||||
@@ -410,6 +420,52 @@ impl ValidatorDefinitions {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|def| def.signing_definition.voting_keystore_password_path())
|
.filter_map(|def| def.signing_definition.voting_keystore_password_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called after loading to run safety checks on all validators
|
||||||
|
pub fn check_all_fee_recipients(
|
||||||
|
&self,
|
||||||
|
global_fee_recipient: Option<Address>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let missing: Vec<&PublicKey> = self
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.filter_map(|def| def.check_fee_recipient(global_fee_recipient))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !missing.is_empty() {
|
||||||
|
let pubkeys = missing
|
||||||
|
.iter()
|
||||||
|
.map(|pk| pk.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return Err(format!(
|
||||||
|
"The following validators are missing a `suggested_fee_recipient`: {}. \
|
||||||
|
Fix this by adding a `suggested_fee_recipient` in the \
|
||||||
|
`validator_definitions.yml` or by supplying a fallback fee \
|
||||||
|
recipient via the `--suggested-fee-recipient` flag.",
|
||||||
|
pubkeys
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Friendly reminder for users using the fallback flag
|
||||||
|
if global_fee_recipient.is_some() {
|
||||||
|
let count = self
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.filter(|d| d.enabled && d.suggested_fee_recipient.is_none())
|
||||||
|
.count();
|
||||||
|
if count > 0 {
|
||||||
|
debug!(
|
||||||
|
"The fallback --suggested-fee-recipient is being used for {} validator(s). \
|
||||||
|
You may alternatively set the fee recipient for each validator individually via `validator_definitions.yml`.",
|
||||||
|
count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform an exhaustive tree search of `dir`, adding any discovered voting keystore paths to
|
/// Perform an exhaustive tree search of `dir`, adding any discovered voting keystore paths to
|
||||||
@@ -485,6 +541,7 @@ pub fn is_voting_keystore(file_name: &str) -> bool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use bls::Keypair;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -682,4 +739,235 @@ mod tests {
|
|||||||
let def: ValidatorDefinition = yaml_serde::from_str(valid_builder_proposals).unwrap();
|
let def: ValidatorDefinition = yaml_serde::from_str(valid_builder_proposals).unwrap();
|
||||||
assert_eq!(def.builder_proposals, Some(true));
|
assert_eq!(def.builder_proposals, Some(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fee_recipient_check_enabled_validator_cases() {
|
||||||
|
let def = ValidatorDefinition {
|
||||||
|
enabled: true,
|
||||||
|
voting_public_key: PublicKey::from_str(
|
||||||
|
"0xaf3c7ddab7e293834710fca2d39d068f884455ede270e0d0293dc818e4f2f0f975355067e8437955cb29aec674e5c9e7"
|
||||||
|
).unwrap(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: None,
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should return Some(pubkey) when no fee recipient is set
|
||||||
|
let check_result = def.check_fee_recipient(None);
|
||||||
|
assert!(check_result.is_some());
|
||||||
|
|
||||||
|
// Should return None since global fee recipient is set
|
||||||
|
let global_fee_recipient =
|
||||||
|
Some(Address::from_str("0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d").unwrap());
|
||||||
|
let check_result = def.check_fee_recipient(global_fee_recipient);
|
||||||
|
assert!(check_result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fee_recipient_check_passes_with_validator_specific() {
|
||||||
|
let def = ValidatorDefinition {
|
||||||
|
enabled: true,
|
||||||
|
voting_public_key: PublicKey::from_str(
|
||||||
|
"0xaf3c7ddab7e293834710fca2d39d068f884455ede270e0d0293dc818e4f2f0f975355067e8437955cb29aec674e5c9e7"
|
||||||
|
).unwrap(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: Some(Address::from_str("0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d").unwrap()),
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should return None because suggested_fee_recipient is set
|
||||||
|
let check_result = def.check_fee_recipient(None);
|
||||||
|
assert!(check_result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fee_recipient_check_skips_disabled_validators() {
|
||||||
|
let def = ValidatorDefinition {
|
||||||
|
enabled: false,
|
||||||
|
voting_public_key: PublicKey::from_str(
|
||||||
|
"0xaf3c7ddab7e293834710fca2d39d068f884455ede270e0d0293dc818e4f2f0f975355067e8437955cb29aec674e5c9e7"
|
||||||
|
).unwrap(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: None,
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should return None because validator is disabled
|
||||||
|
let check_result = def.check_fee_recipient(None);
|
||||||
|
assert!(check_result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_all_fee_recipients_reports_all_missing() {
|
||||||
|
let keypair1 = Keypair::random();
|
||||||
|
let keypair2 = Keypair::random();
|
||||||
|
|
||||||
|
let def1 = ValidatorDefinition {
|
||||||
|
enabled: true,
|
||||||
|
voting_public_key: keypair1.pk.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: None,
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let def2 = ValidatorDefinition {
|
||||||
|
enabled: true,
|
||||||
|
voting_public_key: keypair2.pk.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: None, // Missing recipient
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let defs = ValidatorDefinitions::from(vec![def1, def2]);
|
||||||
|
|
||||||
|
// Should fail because both defs have no fee recipient and no global fee recipient is set
|
||||||
|
let result = defs.check_all_fee_recipients(None);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
|
||||||
|
// Check that both public keys are mentioned in the error message
|
||||||
|
let pk1_string = keypair1.pk.to_string();
|
||||||
|
let pk2_string = keypair2.pk.to_string();
|
||||||
|
|
||||||
|
assert!(err.contains(&pk1_string), "Error message missing pubkey 1");
|
||||||
|
assert!(err.contains(&pk2_string), "Error message missing pubkey 2");
|
||||||
|
assert!(err.contains("are missing a `suggested_fee_recipient`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_all_fee_recipients_passes_all_configured() {
|
||||||
|
let keypair = Keypair::random();
|
||||||
|
let def1 = ValidatorDefinition {
|
||||||
|
enabled: true,
|
||||||
|
voting_public_key: keypair.pk.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: Some(
|
||||||
|
Address::from_str("0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d").unwrap(),
|
||||||
|
),
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let def2 = ValidatorDefinition {
|
||||||
|
enabled: true,
|
||||||
|
voting_public_key: keypair.pk.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: Some(
|
||||||
|
Address::from_str("0xb2e334e71511686bcfe38bb3ee1ad8f6babcc03d").unwrap(),
|
||||||
|
),
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let defs = ValidatorDefinitions::from(vec![def1, def2]);
|
||||||
|
|
||||||
|
// Should pass - all validators have fee recipients
|
||||||
|
assert!(defs.check_all_fee_recipients(None).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_all_fee_recipients_passes_with_global() {
|
||||||
|
let keypair = Keypair::random();
|
||||||
|
let def1 = ValidatorDefinition {
|
||||||
|
enabled: true,
|
||||||
|
voting_public_key: keypair.pk.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: None,
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let def2 = ValidatorDefinition {
|
||||||
|
enabled: true,
|
||||||
|
voting_public_key: keypair.pk.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
graffiti: None,
|
||||||
|
suggested_fee_recipient: None,
|
||||||
|
gas_limit: None,
|
||||||
|
builder_proposals: None,
|
||||||
|
builder_boost_factor: None,
|
||||||
|
prefer_builder_proposals: None,
|
||||||
|
signing_definition: SigningDefinition::LocalKeystore {
|
||||||
|
voting_keystore_path: PathBuf::new(),
|
||||||
|
voting_keystore_password_path: None,
|
||||||
|
voting_keystore_password: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let defs = ValidatorDefinitions::from(vec![def1, def2]);
|
||||||
|
|
||||||
|
// Should pass - global fee recipient is set
|
||||||
|
let global_fee_recipient =
|
||||||
|
Some(Address::from_str("0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d").unwrap());
|
||||||
|
assert!(defs.check_all_fee_recipients(global_fee_recipient).is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,9 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
|||||||
info!(new_validators, "Completed validator discovery");
|
info!(new_validators, "Completed validator discovery");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for all validators' fee recipient
|
||||||
|
validator_defs.check_all_fee_recipients(config.validator_store.fee_recipient)?;
|
||||||
|
|
||||||
let validators = InitializedValidators::from_definitions(
|
let validators = InitializedValidators::from_definitions(
|
||||||
validator_defs,
|
validator_defs,
|
||||||
config.validator_dir.clone(),
|
config.validator_dir.clone(),
|
||||||
|
|||||||
Reference in New Issue
Block a user