diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index aa09211e4..12c84363b 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -223,7 +223,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin num_imported_keystores += 1; let validator_def = - ValidatorDefinition::new_keystore_with_password(&dest_keystore, password_opt) + ValidatorDefinition::new_keystore_with_password(&dest_keystore, password_opt, None) .map_err(|e| format!("Unable to create new validator definition: {:?}", e))?; defs.push(validator_def); diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 5c1b89c49..8e0998532 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -30,6 +30,7 @@ * [Prometheus Metrics](./advanced_metrics.md) * [Advanced Usage](./advanced.md) * [Custom Data Directories](./advanced-datadir.md) + * [Validator Graffiti](./graffiti.md) * [Database Configuration](./advanced_database.md) * [Local Testnets](./local-testnets.md) * [Advanced Networking](./advanced_networking.md) diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index d5cc245cd..033f5d765 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -279,7 +279,8 @@ Typical Responses | 200 { "enable": true, "description": "validator_one", - "deposit_gwei": "32000000000" + "deposit_gwei": "32000000000", + "graffiti": "Mr F was here" }, { "enable": false, diff --git a/book/src/graffiti.md b/book/src/graffiti.md new file mode 100644 index 000000000..d657c9229 --- /dev/null +++ b/book/src/graffiti.md @@ -0,0 +1,62 @@ +# Validator Graffiti + +Lighthouse provides four options for setting validator graffiti. + +### 1. Using the "--graffiti-file" flag on the validator client +Users can specify a file with the `--graffiti-file` flag. This option is useful for dynamically changing graffitis for various use cases (e.g. drawing on the beaconcha.in graffiti wall). This file is loaded once on startup and reloaded everytime a validator is chosen to propose a block. + +Usage: +`lighthouse vc --graffiti-file graffiti_file.txt` + +The file should contain key value pairs corresponding to validator public keys and their associated graffiti. The file can also contain a `default` key for the default case. +``` +default: default_graffiti +public_key1: graffiti1 +public_key2: graffiti2 +... +``` + +Below is an example of a graffiti file: + +``` +default: Lighthouse +0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007: mr f was here +0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477: mr v was here +``` + +Lighthouse will first search for the graffiti corresponding to the public key of the proposing validator, if there are no matches for the public key, then it uses the graffiti corresponding to the default key if present. + +### 2. Setting the graffiti in the `validator_definitions.yml` +Users can set validator specific graffitis in `validator_definitions.yml` with the `graffiti` key. This option is recommended for static setups where the graffitis won't change on every new block proposal. + +Below is an example of the validator_definitions.yml with validator specific graffitis: +``` +--- +- enabled: true + voting_public_key: "0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007" + type: local_keystore + voting_keystore_path: /home/paul/.lighthouse/validators/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007/voting-keystore.json + voting_keystore_password_path: /home/paul/.lighthouse/secrets/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007 + graffiti: "mr f was here" +- enabled: false + voting_public_key: "0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477" + type: local_keystore + voting_keystore_path: /home/paul/.lighthouse/validators/0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477/voting-keystore.json + voting_keystore_password: myStrongpa55word123&$ + graffiti: "somethingprofound" +``` + +### 3. Using the "--graffiti" flag on the validator client +Users can specify a common graffiti for all their validators using the `--graffiti` flag on the validator client. + +### 4. Using the "--graffiti" flag on the beacon node +Users can also specify a common graffiti using the `--graffiti` flag on the beacon node as a common graffiti for all validators. + +Usage: `lighthouse vc --graffiti fortytwo` + +> Note: The order of preference for loading the graffiti is as follows: +> 1. Read from `--graffiti-file` if provided. +> 2. If `--graffiti-file` is not provided or errors, read graffiti from `validator_definitions.yml`. +> 3. If graffiti is not specified in `validator_definitions.yml`, load the graffiti passed in the `--graffiti` flag on the validator client. +> 4. If the `--graffiti` flag on the validator client is not passed, load the graffiti passed in the `--graffiti` flag on the beacon node. +> 4. If the `--graffiti` flag is not passed, load the default Lighthouse graffiti. diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 48643c9e8..466349cf9 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -13,7 +13,7 @@ use std::collections::HashSet; use std::fs::{self, OpenOptions}; use std::io; use std::path::{Path, PathBuf}; -use types::PublicKey; +use types::{graffiti::GraffitiString, PublicKey}; use validator_dir::VOTING_KEYSTORE_FILE; /// The file name for the serialized `ValidatorDefinitions` struct. @@ -66,6 +66,9 @@ pub struct ValidatorDefinition { pub enabled: bool, pub voting_public_key: PublicKey, #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub graffiti: Option, + #[serde(default)] pub description: String, #[serde(flatten)] pub signing_definition: SigningDefinition, @@ -81,6 +84,7 @@ impl ValidatorDefinition { pub fn new_keystore_with_password>( voting_keystore_path: P, voting_keystore_password: Option, + graffiti: Option, ) -> Result { let voting_keystore_path = voting_keystore_path.as_ref().into(); let keystore = @@ -91,6 +95,7 @@ impl ValidatorDefinition { enabled: true, voting_public_key, description: keystore.description().unwrap_or("").to_string(), + graffiti, signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path: None, @@ -227,6 +232,7 @@ impl ValidatorDefinitions { enabled: true, voting_public_key, description: keystore.description().unwrap_or("").to_string(), + graffiti: None, signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path, @@ -347,6 +353,7 @@ pub fn is_voting_keystore(file_name: &str) -> bool { #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; #[test] fn voting_keystore_filename_lighthouse() { @@ -382,4 +389,44 @@ mod tests { assert!(!is_voting_keystore("keystore-0a.json")); assert!(!is_voting_keystore("keystore-cats.json")); } + + #[test] + fn graffiti_checks() { + let no_graffiti = r#"--- + description: "" + enabled: true + type: local_keystore + voting_keystore_path: "" + voting_public_key: "0xaf3c7ddab7e293834710fca2d39d068f884455ede270e0d0293dc818e4f2f0f975355067e8437955cb29aec674e5c9e7" + "#; + let def: ValidatorDefinition = serde_yaml::from_str(&no_graffiti).unwrap(); + assert!(def.graffiti.is_none()); + + let invalid_graffiti = r#"--- + description: "" + enabled: true + type: local_keystore + graffiti: "mrfwasheremrfwasheremrfwasheremrf" + voting_keystore_path: "" + voting_public_key: "0xaf3c7ddab7e293834710fca2d39d068f884455ede270e0d0293dc818e4f2f0f975355067e8437955cb29aec674e5c9e7" + "#; + + let def: Result = serde_yaml::from_str(&invalid_graffiti); + assert!(def.is_err()); + + let valid_graffiti = r#"--- + description: "" + enabled: true + type: local_keystore + graffiti: "mrfwashere" + voting_keystore_path: "" + voting_public_key: "0xaf3c7ddab7e293834710fca2d39d068f884455ede270e0d0293dc818e4f2f0f975355067e8437955cb29aec674e5c9e7" + "#; + + let def: ValidatorDefinition = serde_yaml::from_str(&valid_graffiti).unwrap(); + assert_eq!( + def.graffiti, + Some(GraffitiString::from_str("mrfwashere").unwrap()) + ); + } } diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index 64674e6fc..8994501c8 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -1,5 +1,6 @@ use account_utils::ZeroizeString; use eth2_keystore::Keystore; +use graffiti::GraffitiString; use serde::{Deserialize, Serialize}; pub use crate::lighthouse::Health; @@ -17,6 +18,9 @@ pub struct ValidatorData { pub struct ValidatorRequest { pub enable: bool, pub description: String, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub graffiti: Option, #[serde(with = "serde_utils::quoted_u64")] pub deposit_gwei: u64, } @@ -34,6 +38,9 @@ pub struct CreatedValidator { pub enabled: bool, pub description: String, pub voting_pubkey: PublicKeyBytes, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub graffiti: Option, pub eth1_deposit_tx_data: String, #[serde(with = "serde_utils::quoted_u64")] pub deposit_gwei: u64, @@ -55,4 +62,5 @@ pub struct KeystoreValidatorsPostRequest { pub password: ZeroizeString, pub enable: bool, pub keystore: Keystore, + pub graffiti: Option, } diff --git a/consensus/types/src/graffiti.rs b/consensus/types/src/graffiti.rs index 15cf089c5..c30ec647b 100644 --- a/consensus/types/src/graffiti.rs +++ b/consensus/types/src/graffiti.rs @@ -6,6 +6,7 @@ use regex::bytes::Regex; use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use ssz::{Decode, DecodeError, Encode}; use std::fmt; +use std::str::FromStr; use tree_hash::TreeHash; pub const GRAFFITI_BYTES_LEN: usize = 32; @@ -42,6 +43,49 @@ impl Into<[u8; GRAFFITI_BYTES_LEN]> for Graffiti { } } +#[derive(Debug, Clone, PartialEq, Serialize, Default)] +#[serde(transparent)] +pub struct GraffitiString(String); + +impl FromStr for GraffitiString { + type Err = String; + + fn from_str(s: &str) -> Result { + if s.as_bytes().len() > GRAFFITI_BYTES_LEN { + return Err(format!( + "Graffiti exceeds max length {}", + GRAFFITI_BYTES_LEN + )); + } + Ok(Self(s.to_string())) + } +} + +impl<'de> Deserialize<'de> for GraffitiString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = serde::Deserialize::deserialize(deserializer)?; + GraffitiString::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl Into for GraffitiString { + fn into(self) -> Graffiti { + let graffiti_bytes = self.0.as_bytes(); + let mut graffiti = [0; 32]; + + let graffiti_len = std::cmp::min(graffiti_bytes.len(), 32); + + // Copy the provided bytes over. + // + // Panic-free because `graffiti_bytes.len()` <= `GRAFFITI_BYTES_LEN`. + graffiti[..graffiti_len].copy_from_slice(&graffiti_bytes); + graffiti.into() + } +} + pub mod serde_graffiti { use super::*; diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index 320921e64..baa2ccda3 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -482,6 +482,7 @@ fn validator_import_launchpad() { let expected_def = ValidatorDefinition { enabled: true, description: "".into(), + graffiti: None, voting_public_key: keystore.public_key().unwrap(), signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index d4e42efc6..d302e428c 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -1,4 +1,7 @@ -use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; +use crate::{ + beacon_node_fallback::{BeaconNodeFallback, RequireSynced}, + graffiti_file::GraffitiFile, +}; use crate::{http_metrics::metrics, validator_store::ValidatorStore}; use environment::RuntimeContext; use eth2::types::Graffiti; @@ -17,6 +20,7 @@ pub struct BlockServiceBuilder { beacon_nodes: Option>>, context: Option>, graffiti: Option, + graffiti_file: Option, } impl BlockServiceBuilder { @@ -27,6 +31,7 @@ impl BlockServiceBuilder { beacon_nodes: None, context: None, graffiti: None, + graffiti_file: None, } } @@ -55,6 +60,11 @@ impl BlockServiceBuilder { self } + pub fn graffiti_file(mut self, graffiti_file: Option) -> Self { + self.graffiti_file = graffiti_file; + self + } + pub fn build(self) -> Result, String> { Ok(BlockService { inner: Arc::new(Inner { @@ -71,6 +81,7 @@ impl BlockServiceBuilder { .context .ok_or("Cannot build BlockService without runtime_context")?, graffiti: self.graffiti, + graffiti_file: self.graffiti_file, }), }) } @@ -83,6 +94,7 @@ pub struct Inner { beacon_nodes: Arc>, context: RuntimeContext, graffiti: Option, + graffiti_file: Option, } /// Attempts to produce attestations for any block producer(s) at the start of the epoch. @@ -226,6 +238,19 @@ impl BlockService { .ok_or("Unable to produce randao reveal")? .into(); + let graffiti = self + .graffiti_file + .clone() + .and_then(|mut g| match g.load_graffiti(&validator_pubkey) { + Ok(g) => g, + Err(e) => { + warn!(log, "Failed to read graffiti file"; "error" => ?e); + None + } + }) + .or_else(|| self.validator_store.graffiti(&validator_pubkey)) + .or(self.graffiti); + let randao_reveal_ref = &randao_reveal; let self_ref = &self; let validator_pubkey_ref = &validator_pubkey; @@ -233,7 +258,7 @@ impl BlockService { .beacon_nodes .first_success(RequireSynced::No, |beacon_node| async move { let block = beacon_node - .get_validator_blocks(slot, randao_reveal_ref, self_ref.graffiti.as_ref()) + .get_validator_blocks(slot, randao_reveal_ref, graffiti.as_ref()) .await .map_err(|e| format!("Error from beacon node when producing block: {:?}", e))? .data; @@ -260,6 +285,7 @@ impl BlockService { "Successfully published block"; "deposits" => signed_block.message.body.deposits.len(), "attestations" => signed_block.message.body.attestations.len(), + "graffiti" => ?graffiti.map(|g| g.as_utf8_lossy()), "slot" => signed_block.slot().as_u64(), ); diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index caf295d83..75be32d07 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -102,6 +102,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("GRAFFITI") .takes_value(true) ) + .arg( + Arg::with_name("graffiti-file") + .long("graffiti-file") + .help("Specify a graffiti file to load validator graffitis from.") + .value_name("GRAFFITI-FILE") + .takes_value(true) + .conflicts_with("graffiti") + ) /* REST API related arguments */ .arg( Arg::with_name("http") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index ad23cdb5d..0506565bf 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,3 +1,4 @@ +use crate::graffiti_file::GraffitiFile; use crate::{http_api, http_metrics}; use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; @@ -7,7 +8,7 @@ use directory::{ }; use eth2::types::Graffiti; use serde_derive::{Deserialize, Serialize}; -use slog::{warn, Logger}; +use slog::{info, warn, Logger}; use std::fs; use std::net::Ipv4Addr; use std::path::PathBuf; @@ -35,6 +36,8 @@ pub struct Config { pub init_slashing_protection: bool, /// Graffiti to be inserted everytime we create a block. pub graffiti: Option, + /// Graffiti file to load per validator graffitis. + pub graffiti_file: Option, /// Configuration for the HTTP REST API. pub http_api: http_api::Config, /// Configuration for the HTTP REST API. @@ -60,6 +63,7 @@ impl Default for Config { disable_auto_discover: false, init_slashing_protection: false, graffiti: None, + graffiti_file: None, http_api: <_>::default(), http_metrics: <_>::default(), } @@ -140,6 +144,15 @@ impl Config { config.disable_auto_discover = cli_args.is_present("disable-auto-discover"); config.init_slashing_protection = cli_args.is_present("init-slashing-protection"); + if let Some(graffiti_file_path) = cli_args.value_of("graffiti-file") { + let mut graffiti_file = GraffitiFile::new(graffiti_file_path.into()); + graffiti_file + .read_graffiti_file() + .map_err(|e| format!("Error reading graffiti file: {:?}", e))?; + config.graffiti_file = Some(graffiti_file); + info!(log, "Successfully loaded graffiti file"; "path" => graffiti_file_path); + } + if let Some(input_graffiti) = cli_args.value_of("graffiti") { let graffiti_bytes = input_graffiti.as_bytes(); if graffiti_bytes.len() > GRAFFITI_BYTES_LEN { diff --git a/validator_client/src/graffiti_file.rs b/validator_client/src/graffiti_file.rs new file mode 100644 index 000000000..a16646d2f --- /dev/null +++ b/validator_client/src/graffiti_file.rs @@ -0,0 +1,174 @@ +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::File; +use std::io::{prelude::*, BufReader}; +use std::path::PathBuf; +use std::str::FromStr; + +use bls::blst_implementations::PublicKey; +use types::{graffiti::GraffitiString, Graffiti}; + +#[derive(Debug)] +pub enum Error { + InvalidFile(std::io::Error), + InvalidLine(String), + InvalidPublicKey(String), + InvalidGraffiti(String), +} + +/// Struct to load validator graffitis from file. +/// The graffiti file is expected to have the following structure +/// +/// default: Lighthouse +/// public_key1: graffiti1 +/// public_key2: graffiti2 +/// ... +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GraffitiFile { + graffiti_path: PathBuf, + graffitis: HashMap, + default: Option, +} + +impl GraffitiFile { + pub fn new(graffiti_path: PathBuf) -> Self { + Self { + graffiti_path, + graffitis: HashMap::new(), + default: None, + } + } + + /// Loads the graffiti file and populates the default graffiti and `graffitis` hashmap. + /// Returns the graffiti corresponding to the given public key if present, else returns the + /// default graffiti. + /// + /// Returns an error if loading from the graffiti file fails. + pub fn load_graffiti(&mut self, public_key: &PublicKey) -> Result, Error> { + self.read_graffiti_file()?; + Ok(self.graffitis.get(public_key).copied().or(self.default)) + } + + /// Reads from a graffiti file with the specified format and populates the default value + /// and the hashmap. + /// + /// Returns an error if the file does not exist, or if the format is invalid. + pub fn read_graffiti_file(&mut self) -> Result<(), Error> { + let file = File::open(self.graffiti_path.as_path()).map_err(Error::InvalidFile)?; + let reader = BufReader::new(file); + + let lines = reader.lines(); + + for line in lines { + let line = line.map_err(|e| Error::InvalidLine(e.to_string()))?; + let (pk_opt, graffiti) = read_line(&line)?; + match pk_opt { + Some(pk) => { + self.graffitis.insert(pk, graffiti); + } + None => self.default = Some(graffiti), + } + } + Ok(()) + } +} + +/// Parses a line from the graffiti file. +/// +/// `Ok((None, graffiti))` represents the graffiti for the default key. +/// `Ok((Some(pk), graffiti))` represents graffiti for the public key `pk`. +/// Returns an error if the line is in the wrong format or does not contain a valid public key or graffiti. +fn read_line(line: &str) -> Result<(Option, Graffiti), Error> { + if let Some(i) = line.find(':') { + let (key, value) = line.split_at(i); + // Note: `value.len() >=1` so `value[1..]` is safe + let graffiti = GraffitiString::from_str(value[1..].trim()) + .map_err(Error::InvalidGraffiti)? + .into(); + if key == "default" { + Ok((None, graffiti)) + } else { + let pk = PublicKey::from_str(&key).map_err(Error::InvalidPublicKey)?; + Ok((Some(pk), graffiti)) + } + } else { + Err(Error::InvalidLine(format!("Missing delimiter: {}", line))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bls::Keypair; + use std::io::LineWriter; + use tempfile::TempDir; + + const DEFAULT_GRAFFITI: &str = "lighthouse"; + const CUSTOM_GRAFFITI1: &str = "custom-graffiti1"; + const CUSTOM_GRAFFITI2: &str = "graffitiwall:720:641:#ffff00"; + const EMPTY_GRAFFITI: &str = ""; + const PK1: &str = "0x800012708dc03f611751aad7a43a082142832b5c1aceed07ff9b543cf836381861352aa923c70eeb02018b638aa306aa"; + const PK2: &str = "0x80001866ce324de7d80ec73be15e2d064dcf121adf1b34a0d679f2b9ecbab40ce021e03bb877e1a2fe72eaaf475e6e21"; + const PK3: &str = "0x9035d41a8bc11b08c17d0d93d876087958c9d055afe86fce558e3b988d92434769c8d50b0b463708db80c6aae1160c02"; + + // Create a graffiti file in the required format and return a path to the file. + fn create_graffiti_file() -> PathBuf { + let temp = TempDir::new().unwrap(); + let pk1 = PublicKey::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap(); + let pk2 = PublicKey::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap(); + let pk3 = PublicKey::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap(); + + let file_name = temp.into_path().join("graffiti.txt"); + + let file = File::create(&file_name).unwrap(); + let mut graffiti_file = LineWriter::new(file); + graffiti_file + .write_all(format!("default: {}\n", DEFAULT_GRAFFITI).as_bytes()) + .unwrap(); + graffiti_file + .write_all(format!("{}: {}\n", pk1.to_hex_string(), CUSTOM_GRAFFITI1).as_bytes()) + .unwrap(); + graffiti_file + .write_all(format!("{}: {}\n", pk2.to_hex_string(), CUSTOM_GRAFFITI2).as_bytes()) + .unwrap(); + graffiti_file + .write_all(format!("{}:{}\n", pk3.to_hex_string(), EMPTY_GRAFFITI).as_bytes()) + .unwrap(); + graffiti_file.flush().unwrap(); + file_name + } + + #[test] + fn test_load_graffiti() { + let graffiti_file_path = create_graffiti_file(); + let mut gf = GraffitiFile::new(graffiti_file_path); + + let pk1 = PublicKey::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap(); + let pk2 = PublicKey::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap(); + let pk3 = PublicKey::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap(); + + // Read once + gf.read_graffiti_file().unwrap(); + + assert_eq!( + gf.load_graffiti(&pk1).unwrap().unwrap(), + GraffitiString::from_str(CUSTOM_GRAFFITI1).unwrap().into() + ); + assert_eq!( + gf.load_graffiti(&pk2).unwrap().unwrap(), + GraffitiString::from_str(CUSTOM_GRAFFITI2).unwrap().into() + ); + + assert_eq!( + gf.load_graffiti(&pk3).unwrap().unwrap(), + GraffitiString::from_str(EMPTY_GRAFFITI).unwrap().into() + ); + + // Random pk should return the default graffiti + let random_pk = Keypair::random().pk; + assert_eq!( + gf.load_graffiti(&random_pk).unwrap().unwrap(), + GraffitiString::from_str(DEFAULT_GRAFFITI).unwrap().into() + ); + } +} diff --git a/validator_client/src/http_api/create_validator.rs b/validator_client/src/http_api/create_validator.rs index 2834a1839..5a6da2bc9 100644 --- a/validator_client/src/http_api/create_validator.rs +++ b/validator_client/src/http_api/create_validator.rs @@ -133,7 +133,12 @@ pub async fn create_validators, T: 'static + SlotClock, E: EthSpe drop(validator_dir); validator_store - .add_validator_keystore(voting_keystore_path, voting_password_string, request.enable) + .add_validator_keystore( + voting_keystore_path, + voting_password_string, + request.enable, + request.graffiti.clone(), + ) .await .map_err(|e| { warp_utils::reject::custom_server_error(format!( @@ -145,6 +150,7 @@ pub async fn create_validators, T: 'static + SlotClock, E: EthSpe validators.push(api_types::CreatedValidator { enabled: request.enable, description: request.description.clone(), + graffiti: request.graffiti.clone(), voting_pubkey, eth1_deposit_tx_data: serde_utils::hex::encode(ð1_deposit_data.rlp), deposit_gwei: request.deposit_gwei, diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index a75cc9703..ccf465f53 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -383,6 +383,7 @@ pub fn serve( let voting_keystore_path = validator_dir.voting_keystore_path(); drop(validator_dir); let voting_password = body.password.clone(); + let graffiti = body.graffiti.clone(); let validator_def = { if let Some(runtime) = runtime.upgrade() { @@ -391,6 +392,7 @@ pub fn serve( voting_keystore_path, voting_password, body.enable, + graffiti, )) .map_err(|e| { warp_utils::reject::custom_server_error(format!( diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index f9ebec71a..d7fcc38c1 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -210,6 +210,7 @@ impl ApiTester { .map(|i| ValidatorRequest { enable: !s.disabled.contains(&i), description: format!("boi #{}", i), + graffiti: None, deposit_gwei: E::default_spec().max_effective_balance, }) .collect::>(); @@ -339,6 +340,7 @@ impl ApiTester { .unwrap() .into(), keystore, + graffiti: None, }; self.client @@ -355,6 +357,7 @@ impl ApiTester { .unwrap() .into(), keystore, + graffiti: None, }; let response = self diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 92c07d4b9..731a0b1a9 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io; use std::path::PathBuf; -use types::{Keypair, PublicKey}; +use types::{Graffiti, Keypair, PublicKey}; use crate::key_cache; use crate::key_cache::KeyCache; @@ -86,6 +86,7 @@ pub enum SigningMethod { /// A validator that is ready to sign messages. pub struct InitializedValidator { signing_method: SigningMethod, + graffiti: Option, } impl InitializedValidator { @@ -213,6 +214,7 @@ impl InitializedValidator { voting_keystore: voting_keystore.clone(), voting_keypair, }, + graffiti: def.graffiti.map(Into::into), }) } } @@ -363,6 +365,11 @@ impl InitializedValidators { .map(|def| def.enabled) } + /// Returns the `graffiti` for a given public key specified in the `ValidatorDefinitions`. + pub fn graffiti(&self, public_key: &PublicKey) -> Option { + self.validators.get(public_key).and_then(|v| v.graffiti) + } + /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled` values. /// /// ## Notes @@ -533,7 +540,7 @@ impl InitializedValidators { info!( self.log, "Enabled validator"; - "voting_pubkey" => format!("{:?}", def.voting_public_key) + "voting_pubkey" => format!("{:?}", def.voting_public_key), ); if let Some(lockfile_path) = existing_lockfile_path { diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index edc873efe..fb3e81e33 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -6,6 +6,7 @@ mod cli; mod config; mod duties_service; mod fork_service; +mod graffiti_file; mod http_metrics; mod initialized_validators; mod key_cache; @@ -304,6 +305,7 @@ impl ProductionValidatorClient { .beacon_nodes(beacon_nodes.clone()) .runtime_context(context.service_context("block".into())) .graffiti(config.graffiti) + .graffiti_file(config.graffiti_file.clone()) .build()?; let attestation_service = AttestationServiceBuilder::new() diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index f152f411f..c23cc1ba5 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -10,8 +10,9 @@ use std::path::Path; use std::sync::Arc; use tempfile::TempDir; use types::{ - Attestation, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, Fork, Hash256, Keypair, PublicKey, - SelectionProof, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedRoot, Slot, + graffiti::GraffitiString, Attestation, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, Fork, + Graffiti, Hash256, Keypair, PublicKey, SelectionProof, Signature, SignedAggregateAndProof, + SignedBeaconBlock, SignedRoot, Slot, }; use validator_dir::ValidatorDir; @@ -95,10 +96,14 @@ impl ValidatorStore { voting_keystore_path: P, password: ZeroizeString, enable: bool, + graffiti: Option, ) -> Result { - let mut validator_def = - ValidatorDefinition::new_keystore_with_password(voting_keystore_path, Some(password)) - .map_err(|e| format!("failed to create validator definitions: {:?}", e))?; + let mut validator_def = ValidatorDefinition::new_keystore_with_password( + voting_keystore_path, + Some(password), + graffiti.map(Into::into), + ) + .map_err(|e| format!("failed to create validator definitions: {:?}", e))?; self.slashing_protection .register_validator(&validator_def.voting_public_key) @@ -148,6 +153,10 @@ impl ValidatorStore { }) } + pub fn graffiti(&self, validator_pubkey: &PublicKey) -> Option { + self.validators.read().graffiti(validator_pubkey) + } + pub fn sign_block( &self, validator_pubkey: &PublicKey,