Set graffiti per validator (#2044)

## Issue Addressed

Resolves #1944 

## Proposed Changes

Adds a "graffiti" key to the `validator_definitions.yml`. Setting the key will override anything passed through the validator `--graffiti` flag. 
Returns an error if the value for the graffiti key is > 32 bytes instead of silently truncating.
This commit is contained in:
Pawan Dhananjay 2021-03-02 22:35:46 +00:00
parent 1c507c588e
commit da8791abd7
18 changed files with 428 additions and 14 deletions

View File

@ -223,7 +223,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
num_imported_keystores += 1; num_imported_keystores += 1;
let validator_def = 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))?; .map_err(|e| format!("Unable to create new validator definition: {:?}", e))?;
defs.push(validator_def); defs.push(validator_def);

View File

@ -30,6 +30,7 @@
* [Prometheus Metrics](./advanced_metrics.md) * [Prometheus Metrics](./advanced_metrics.md)
* [Advanced Usage](./advanced.md) * [Advanced Usage](./advanced.md)
* [Custom Data Directories](./advanced-datadir.md) * [Custom Data Directories](./advanced-datadir.md)
* [Validator Graffiti](./graffiti.md)
* [Database Configuration](./advanced_database.md) * [Database Configuration](./advanced_database.md)
* [Local Testnets](./local-testnets.md) * [Local Testnets](./local-testnets.md)
* [Advanced Networking](./advanced_networking.md) * [Advanced Networking](./advanced_networking.md)

View File

@ -279,7 +279,8 @@ Typical Responses | 200
{ {
"enable": true, "enable": true,
"description": "validator_one", "description": "validator_one",
"deposit_gwei": "32000000000" "deposit_gwei": "32000000000",
"graffiti": "Mr F was here"
}, },
{ {
"enable": false, "enable": false,

62
book/src/graffiti.md Normal file
View File

@ -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.

View File

@ -13,7 +13,7 @@ use std::collections::HashSet;
use std::fs::{self, OpenOptions}; use std::fs::{self, OpenOptions};
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use types::PublicKey; use types::{graffiti::GraffitiString, PublicKey};
use validator_dir::VOTING_KEYSTORE_FILE; use validator_dir::VOTING_KEYSTORE_FILE;
/// The file name for the serialized `ValidatorDefinitions` struct. /// The file name for the serialized `ValidatorDefinitions` struct.
@ -66,6 +66,9 @@ pub struct ValidatorDefinition {
pub enabled: bool, pub enabled: bool,
pub voting_public_key: PublicKey, pub voting_public_key: PublicKey,
#[serde(default)] #[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub graffiti: Option<GraffitiString>,
#[serde(default)]
pub description: String, pub description: String,
#[serde(flatten)] #[serde(flatten)]
pub signing_definition: SigningDefinition, pub signing_definition: SigningDefinition,
@ -81,6 +84,7 @@ impl ValidatorDefinition {
pub fn new_keystore_with_password<P: AsRef<Path>>( pub fn new_keystore_with_password<P: AsRef<Path>>(
voting_keystore_path: P, voting_keystore_path: P,
voting_keystore_password: Option<ZeroizeString>, voting_keystore_password: Option<ZeroizeString>,
graffiti: Option<GraffitiString>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let voting_keystore_path = voting_keystore_path.as_ref().into(); let voting_keystore_path = voting_keystore_path.as_ref().into();
let keystore = let keystore =
@ -91,6 +95,7 @@ impl ValidatorDefinition {
enabled: true, enabled: true,
voting_public_key, voting_public_key,
description: keystore.description().unwrap_or("").to_string(), description: keystore.description().unwrap_or("").to_string(),
graffiti,
signing_definition: SigningDefinition::LocalKeystore { signing_definition: SigningDefinition::LocalKeystore {
voting_keystore_path, voting_keystore_path,
voting_keystore_password_path: None, voting_keystore_password_path: None,
@ -227,6 +232,7 @@ impl ValidatorDefinitions {
enabled: true, enabled: true,
voting_public_key, voting_public_key,
description: keystore.description().unwrap_or("").to_string(), description: keystore.description().unwrap_or("").to_string(),
graffiti: None,
signing_definition: SigningDefinition::LocalKeystore { signing_definition: SigningDefinition::LocalKeystore {
voting_keystore_path, voting_keystore_path,
voting_keystore_password_path, voting_keystore_password_path,
@ -347,6 +353,7 @@ pub fn is_voting_keystore(file_name: &str) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::str::FromStr;
#[test] #[test]
fn voting_keystore_filename_lighthouse() { fn voting_keystore_filename_lighthouse() {
@ -382,4 +389,44 @@ mod tests {
assert!(!is_voting_keystore("keystore-0a.json")); assert!(!is_voting_keystore("keystore-0a.json"));
assert!(!is_voting_keystore("keystore-cats.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<ValidatorDefinition, _> = 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())
);
}
} }

View File

@ -1,5 +1,6 @@
use account_utils::ZeroizeString; use account_utils::ZeroizeString;
use eth2_keystore::Keystore; use eth2_keystore::Keystore;
use graffiti::GraffitiString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use crate::lighthouse::Health; pub use crate::lighthouse::Health;
@ -17,6 +18,9 @@ pub struct ValidatorData {
pub struct ValidatorRequest { pub struct ValidatorRequest {
pub enable: bool, pub enable: bool,
pub description: String, pub description: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub graffiti: Option<GraffitiString>,
#[serde(with = "serde_utils::quoted_u64")] #[serde(with = "serde_utils::quoted_u64")]
pub deposit_gwei: u64, pub deposit_gwei: u64,
} }
@ -34,6 +38,9 @@ pub struct CreatedValidator {
pub enabled: bool, pub enabled: bool,
pub description: String, pub description: String,
pub voting_pubkey: PublicKeyBytes, pub voting_pubkey: PublicKeyBytes,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub graffiti: Option<GraffitiString>,
pub eth1_deposit_tx_data: String, pub eth1_deposit_tx_data: String,
#[serde(with = "serde_utils::quoted_u64")] #[serde(with = "serde_utils::quoted_u64")]
pub deposit_gwei: u64, pub deposit_gwei: u64,
@ -55,4 +62,5 @@ pub struct KeystoreValidatorsPostRequest {
pub password: ZeroizeString, pub password: ZeroizeString,
pub enable: bool, pub enable: bool,
pub keystore: Keystore, pub keystore: Keystore,
pub graffiti: Option<GraffitiString>,
} }

View File

@ -6,6 +6,7 @@ use regex::bytes::Regex;
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
use ssz::{Decode, DecodeError, Encode}; use ssz::{Decode, DecodeError, Encode};
use std::fmt; use std::fmt;
use std::str::FromStr;
use tree_hash::TreeHash; use tree_hash::TreeHash;
pub const GRAFFITI_BYTES_LEN: usize = 32; 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<Self, Self::Err> {
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = serde::Deserialize::deserialize(deserializer)?;
GraffitiString::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl Into<Graffiti> 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 { pub mod serde_graffiti {
use super::*; use super::*;

View File

@ -482,6 +482,7 @@ fn validator_import_launchpad() {
let expected_def = ValidatorDefinition { let expected_def = ValidatorDefinition {
enabled: true, enabled: true,
description: "".into(), description: "".into(),
graffiti: None,
voting_public_key: keystore.public_key().unwrap(), voting_public_key: keystore.public_key().unwrap(),
signing_definition: SigningDefinition::LocalKeystore { signing_definition: SigningDefinition::LocalKeystore {
voting_keystore_path, voting_keystore_path,

View File

@ -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 crate::{http_metrics::metrics, validator_store::ValidatorStore};
use environment::RuntimeContext; use environment::RuntimeContext;
use eth2::types::Graffiti; use eth2::types::Graffiti;
@ -17,6 +20,7 @@ pub struct BlockServiceBuilder<T, E: EthSpec> {
beacon_nodes: Option<Arc<BeaconNodeFallback<T, E>>>, beacon_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
context: Option<RuntimeContext<E>>, context: Option<RuntimeContext<E>>,
graffiti: Option<Graffiti>, graffiti: Option<Graffiti>,
graffiti_file: Option<GraffitiFile>,
} }
impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> { impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
@ -27,6 +31,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
beacon_nodes: None, beacon_nodes: None,
context: None, context: None,
graffiti: None, graffiti: None,
graffiti_file: None,
} }
} }
@ -55,6 +60,11 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
self self
} }
pub fn graffiti_file(mut self, graffiti_file: Option<GraffitiFile>) -> Self {
self.graffiti_file = graffiti_file;
self
}
pub fn build(self) -> Result<BlockService<T, E>, String> { pub fn build(self) -> Result<BlockService<T, E>, String> {
Ok(BlockService { Ok(BlockService {
inner: Arc::new(Inner { inner: Arc::new(Inner {
@ -71,6 +81,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
.context .context
.ok_or("Cannot build BlockService without runtime_context")?, .ok_or("Cannot build BlockService without runtime_context")?,
graffiti: self.graffiti, graffiti: self.graffiti,
graffiti_file: self.graffiti_file,
}), }),
}) })
} }
@ -83,6 +94,7 @@ pub struct Inner<T, E: EthSpec> {
beacon_nodes: Arc<BeaconNodeFallback<T, E>>, beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
context: RuntimeContext<E>, context: RuntimeContext<E>,
graffiti: Option<Graffiti>, graffiti: Option<Graffiti>,
graffiti_file: Option<GraffitiFile>,
} }
/// Attempts to produce attestations for any block producer(s) at the start of the epoch. /// Attempts to produce attestations for any block producer(s) at the start of the epoch.
@ -226,6 +238,19 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
.ok_or("Unable to produce randao reveal")? .ok_or("Unable to produce randao reveal")?
.into(); .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 randao_reveal_ref = &randao_reveal;
let self_ref = &self; let self_ref = &self;
let validator_pubkey_ref = &validator_pubkey; let validator_pubkey_ref = &validator_pubkey;
@ -233,7 +258,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
.beacon_nodes .beacon_nodes
.first_success(RequireSynced::No, |beacon_node| async move { .first_success(RequireSynced::No, |beacon_node| async move {
let block = beacon_node 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 .await
.map_err(|e| format!("Error from beacon node when producing block: {:?}", e))? .map_err(|e| format!("Error from beacon node when producing block: {:?}", e))?
.data; .data;
@ -260,6 +285,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
"Successfully published block"; "Successfully published block";
"deposits" => signed_block.message.body.deposits.len(), "deposits" => signed_block.message.body.deposits.len(),
"attestations" => signed_block.message.body.attestations.len(), "attestations" => signed_block.message.body.attestations.len(),
"graffiti" => ?graffiti.map(|g| g.as_utf8_lossy()),
"slot" => signed_block.slot().as_u64(), "slot" => signed_block.slot().as_u64(),
); );

View File

@ -102,6 +102,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.value_name("GRAFFITI") .value_name("GRAFFITI")
.takes_value(true) .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 */ /* REST API related arguments */
.arg( .arg(
Arg::with_name("http") Arg::with_name("http")

View File

@ -1,3 +1,4 @@
use crate::graffiti_file::GraffitiFile;
use crate::{http_api, http_metrics}; use crate::{http_api, http_metrics};
use clap::ArgMatches; use clap::ArgMatches;
use clap_utils::{parse_optional, parse_required}; use clap_utils::{parse_optional, parse_required};
@ -7,7 +8,7 @@ use directory::{
}; };
use eth2::types::Graffiti; use eth2::types::Graffiti;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use slog::{warn, Logger}; use slog::{info, warn, Logger};
use std::fs; use std::fs;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::path::PathBuf; use std::path::PathBuf;
@ -35,6 +36,8 @@ pub struct Config {
pub init_slashing_protection: bool, pub init_slashing_protection: bool,
/// Graffiti to be inserted everytime we create a block. /// Graffiti to be inserted everytime we create a block.
pub graffiti: Option<Graffiti>, pub graffiti: Option<Graffiti>,
/// Graffiti file to load per validator graffitis.
pub graffiti_file: Option<GraffitiFile>,
/// Configuration for the HTTP REST API. /// Configuration for the HTTP REST API.
pub http_api: http_api::Config, pub http_api: http_api::Config,
/// Configuration for the HTTP REST API. /// Configuration for the HTTP REST API.
@ -60,6 +63,7 @@ impl Default for Config {
disable_auto_discover: false, disable_auto_discover: false,
init_slashing_protection: false, init_slashing_protection: false,
graffiti: None, graffiti: None,
graffiti_file: None,
http_api: <_>::default(), http_api: <_>::default(),
http_metrics: <_>::default(), http_metrics: <_>::default(),
} }
@ -140,6 +144,15 @@ impl Config {
config.disable_auto_discover = cli_args.is_present("disable-auto-discover"); config.disable_auto_discover = cli_args.is_present("disable-auto-discover");
config.init_slashing_protection = cli_args.is_present("init-slashing-protection"); 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") { if let Some(input_graffiti) = cli_args.value_of("graffiti") {
let graffiti_bytes = input_graffiti.as_bytes(); let graffiti_bytes = input_graffiti.as_bytes();
if graffiti_bytes.len() > GRAFFITI_BYTES_LEN { if graffiti_bytes.len() > GRAFFITI_BYTES_LEN {

View File

@ -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<PublicKey, Graffiti>,
default: Option<Graffiti>,
}
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<Option<Graffiti>, 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<PublicKey>, 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()
);
}
}

View File

@ -133,7 +133,12 @@ pub async fn create_validators<P: AsRef<Path>, T: 'static + SlotClock, E: EthSpe
drop(validator_dir); drop(validator_dir);
validator_store 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 .await
.map_err(|e| { .map_err(|e| {
warp_utils::reject::custom_server_error(format!( warp_utils::reject::custom_server_error(format!(
@ -145,6 +150,7 @@ pub async fn create_validators<P: AsRef<Path>, T: 'static + SlotClock, E: EthSpe
validators.push(api_types::CreatedValidator { validators.push(api_types::CreatedValidator {
enabled: request.enable, enabled: request.enable,
description: request.description.clone(), description: request.description.clone(),
graffiti: request.graffiti.clone(),
voting_pubkey, voting_pubkey,
eth1_deposit_tx_data: serde_utils::hex::encode(&eth1_deposit_data.rlp), eth1_deposit_tx_data: serde_utils::hex::encode(&eth1_deposit_data.rlp),
deposit_gwei: request.deposit_gwei, deposit_gwei: request.deposit_gwei,

View File

@ -383,6 +383,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
let voting_keystore_path = validator_dir.voting_keystore_path(); let voting_keystore_path = validator_dir.voting_keystore_path();
drop(validator_dir); drop(validator_dir);
let voting_password = body.password.clone(); let voting_password = body.password.clone();
let graffiti = body.graffiti.clone();
let validator_def = { let validator_def = {
if let Some(runtime) = runtime.upgrade() { if let Some(runtime) = runtime.upgrade() {
@ -391,6 +392,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
voting_keystore_path, voting_keystore_path,
voting_password, voting_password,
body.enable, body.enable,
graffiti,
)) ))
.map_err(|e| { .map_err(|e| {
warp_utils::reject::custom_server_error(format!( warp_utils::reject::custom_server_error(format!(

View File

@ -210,6 +210,7 @@ impl ApiTester {
.map(|i| ValidatorRequest { .map(|i| ValidatorRequest {
enable: !s.disabled.contains(&i), enable: !s.disabled.contains(&i),
description: format!("boi #{}", i), description: format!("boi #{}", i),
graffiti: None,
deposit_gwei: E::default_spec().max_effective_balance, deposit_gwei: E::default_spec().max_effective_balance,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -339,6 +340,7 @@ impl ApiTester {
.unwrap() .unwrap()
.into(), .into(),
keystore, keystore,
graffiti: None,
}; };
self.client self.client
@ -355,6 +357,7 @@ impl ApiTester {
.unwrap() .unwrap()
.into(), .into(),
keystore, keystore,
graffiti: None,
}; };
let response = self let response = self

View File

@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet};
use std::fs::File; use std::fs::File;
use std::io; use std::io;
use std::path::PathBuf; use std::path::PathBuf;
use types::{Keypair, PublicKey}; use types::{Graffiti, Keypair, PublicKey};
use crate::key_cache; use crate::key_cache;
use crate::key_cache::KeyCache; use crate::key_cache::KeyCache;
@ -86,6 +86,7 @@ pub enum SigningMethod {
/// A validator that is ready to sign messages. /// A validator that is ready to sign messages.
pub struct InitializedValidator { pub struct InitializedValidator {
signing_method: SigningMethod, signing_method: SigningMethod,
graffiti: Option<Graffiti>,
} }
impl InitializedValidator { impl InitializedValidator {
@ -213,6 +214,7 @@ impl InitializedValidator {
voting_keystore: voting_keystore.clone(), voting_keystore: voting_keystore.clone(),
voting_keypair, voting_keypair,
}, },
graffiti: def.graffiti.map(Into::into),
}) })
} }
} }
@ -363,6 +365,11 @@ impl InitializedValidators {
.map(|def| def.enabled) .map(|def| def.enabled)
} }
/// Returns the `graffiti` for a given public key specified in the `ValidatorDefinitions`.
pub fn graffiti(&self, public_key: &PublicKey) -> Option<Graffiti> {
self.validators.get(public_key).and_then(|v| v.graffiti)
}
/// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled` values. /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled` values.
/// ///
/// ## Notes /// ## Notes
@ -533,7 +540,7 @@ impl InitializedValidators {
info!( info!(
self.log, self.log,
"Enabled validator"; "Enabled validator";
"voting_pubkey" => format!("{:?}", def.voting_public_key) "voting_pubkey" => format!("{:?}", def.voting_public_key),
); );
if let Some(lockfile_path) = existing_lockfile_path { if let Some(lockfile_path) = existing_lockfile_path {

View File

@ -6,6 +6,7 @@ mod cli;
mod config; mod config;
mod duties_service; mod duties_service;
mod fork_service; mod fork_service;
mod graffiti_file;
mod http_metrics; mod http_metrics;
mod initialized_validators; mod initialized_validators;
mod key_cache; mod key_cache;
@ -304,6 +305,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
.beacon_nodes(beacon_nodes.clone()) .beacon_nodes(beacon_nodes.clone())
.runtime_context(context.service_context("block".into())) .runtime_context(context.service_context("block".into()))
.graffiti(config.graffiti) .graffiti(config.graffiti)
.graffiti_file(config.graffiti_file.clone())
.build()?; .build()?;
let attestation_service = AttestationServiceBuilder::new() let attestation_service = AttestationServiceBuilder::new()

View File

@ -10,8 +10,9 @@ use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tempfile::TempDir; use tempfile::TempDir;
use types::{ use types::{
Attestation, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, Fork, Hash256, Keypair, PublicKey, graffiti::GraffitiString, Attestation, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, Fork,
SelectionProof, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedRoot, Slot, Graffiti, Hash256, Keypair, PublicKey, SelectionProof, Signature, SignedAggregateAndProof,
SignedBeaconBlock, SignedRoot, Slot,
}; };
use validator_dir::ValidatorDir; use validator_dir::ValidatorDir;
@ -95,10 +96,14 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
voting_keystore_path: P, voting_keystore_path: P,
password: ZeroizeString, password: ZeroizeString,
enable: bool, enable: bool,
graffiti: Option<GraffitiString>,
) -> Result<ValidatorDefinition, String> { ) -> Result<ValidatorDefinition, String> {
let mut validator_def = let mut validator_def = ValidatorDefinition::new_keystore_with_password(
ValidatorDefinition::new_keystore_with_password(voting_keystore_path, Some(password)) voting_keystore_path,
.map_err(|e| format!("failed to create validator definitions: {:?}", e))?; Some(password),
graffiti.map(Into::into),
)
.map_err(|e| format!("failed to create validator definitions: {:?}", e))?;
self.slashing_protection self.slashing_protection
.register_validator(&validator_def.voting_public_key) .register_validator(&validator_def.voting_public_key)
@ -148,6 +153,10 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
}) })
} }
pub fn graffiti(&self, validator_pubkey: &PublicKey) -> Option<Graffiti> {
self.validators.read().graffiti(validator_pubkey)
}
pub fn sign_block( pub fn sign_block(
&self, &self,
validator_pubkey: &PublicKey, validator_pubkey: &PublicKey,