-
Notifications
You must be signed in to change notification settings - Fork 957
added check for fee recipient per validator and added unit tests #8454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from all commits
4c0dbc5
a5443ad
5fe8cb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,7 @@ use std::collections::HashSet; | |||||
| use std::fs::{self, File, create_dir_all}; | ||||||
| use std::io; | ||||||
| use std::path::{Path, PathBuf}; | ||||||
| use tracing::error; | ||||||
| use tracing::{error, info}; | ||||||
| use types::{Address, graffiti::GraffitiString}; | ||||||
| use validator_dir::VOTING_KEYSTORE_FILE; | ||||||
| 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 | ||||||
|
|
@@ -410,6 +420,52 @@ impl ValidatorDefinitions { | |||||
| .iter() | ||||||
| .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 your \ | ||||||
| `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 { | ||||||
| info!( | ||||||
| "The fallback --suggested-fee-recipient is being used for {} validator(s). \ | ||||||
| Consider setting the fee recipient for each validator individually via `validator_definitions.yml`.", | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is about "reducing the severity" of the log, because if users only use the This is a my two cent, keep to hear from the author and other developers about this. (e.g., maybe even shift to debug log)?
Suggested change
|
||||||
| count | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Ok(()) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Perform an exhaustive tree search of `dir`, adding any discovered voting keystore paths to | ||||||
|
|
@@ -486,6 +542,7 @@ pub fn is_voting_keystore(file_name: &str) -> bool { | |||||
| mod tests { | ||||||
| use super::*; | ||||||
| use std::str::FromStr; | ||||||
| use bls::Keypair; | ||||||
|
|
||||||
| #[test] | ||||||
| fn voting_keystore_filename_lighthouse() { | ||||||
|
|
@@ -682,4 +739,250 @@ mod tests { | |||||
| let def: ValidatorDefinition = serde_yaml::from_str(valid_builder_proposals).unwrap(); | ||||||
| assert_eq!(def.builder_proposals, Some(true)); | ||||||
| } | ||||||
|
|
||||||
| #[test] | ||||||
| fn fee_recipient_check_fails_when_missing() { | ||||||
| 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()); | ||||||
| } | ||||||
|
|
||||||
| #[test] | ||||||
| fn fee_recipient_check_passes_with_global_flag() { | ||||||
| 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 None since global fee recipient is set | ||||||
| let global_fee = Some(Address::from_str("0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d").unwrap()); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just a nit for better clarity
Suggested change
|
||||||
| let check_result = def.check_fee_recipient(global_fee); | ||||||
| assert!(check_result.is_none()); | ||||||
|
Comment on lines
+791
to
+792
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For succinctness, we can combine this with the test Edit: I believe we can even go "all out" for succinctness, to define a general |
||||||
| } | ||||||
|
|
||||||
| #[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, // No fee 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, | ||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
| // 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 = Some(Address::from_str("0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d").unwrap()); | ||||||
| assert!(defs.check_all_fee_recipients(global_fee).is_ok()); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel that
the validator_definitions.ymlreads better here, but feel free to take it or leave it