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:
parent
1c507c588e
commit
da8791abd7
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
62
book/src/graffiti.md
Normal file
62
book/src/graffiti.md
Normal 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.
|
@ -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<GraffitiString>,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(flatten)]
|
||||
pub signing_definition: SigningDefinition,
|
||||
@ -81,6 +84,7 @@ impl ValidatorDefinition {
|
||||
pub fn new_keystore_with_password<P: AsRef<Path>>(
|
||||
voting_keystore_path: P,
|
||||
voting_keystore_password: Option<ZeroizeString>,
|
||||
graffiti: Option<GraffitiString>,
|
||||
) -> Result<Self, Error> {
|
||||
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<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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<GraffitiString>,
|
||||
#[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<GraffitiString>,
|
||||
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<GraffitiString>,
|
||||
}
|
||||
|
@ -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<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 {
|
||||
use super::*;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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<T, E: EthSpec> {
|
||||
beacon_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
|
||||
context: Option<RuntimeContext<E>>,
|
||||
graffiti: Option<Graffiti>,
|
||||
graffiti_file: Option<GraffitiFile>,
|
||||
}
|
||||
|
||||
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,
|
||||
context: None,
|
||||
graffiti: None,
|
||||
graffiti_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +60,11 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
|
||||
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> {
|
||||
Ok(BlockService {
|
||||
inner: Arc::new(Inner {
|
||||
@ -71,6 +81,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
|
||||
.context
|
||||
.ok_or("Cannot build BlockService without runtime_context")?,
|
||||
graffiti: self.graffiti,
|
||||
graffiti_file: self.graffiti_file,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@ -83,6 +94,7 @@ pub struct Inner<T, E: EthSpec> {
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
|
||||
context: RuntimeContext<E>,
|
||||
graffiti: Option<Graffiti>,
|
||||
graffiti_file: Option<GraffitiFile>,
|
||||
}
|
||||
|
||||
/// 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")?
|
||||
.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<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
|
||||
.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<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
|
||||
"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(),
|
||||
);
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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>,
|
||||
/// Graffiti file to load per validator graffitis.
|
||||
pub graffiti_file: Option<GraffitiFile>,
|
||||
/// 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 {
|
||||
|
174
validator_client/src/graffiti_file.rs
Normal file
174
validator_client/src/graffiti_file.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
@ -133,7 +133,12 @@ pub async fn create_validators<P: AsRef<Path>, 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<P: AsRef<Path>, 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,
|
||||
|
@ -383,6 +383,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
|
||||
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<T: 'static + SlotClock + Clone, E: EthSpec>(
|
||||
voting_keystore_path,
|
||||
voting_password,
|
||||
body.enable,
|
||||
graffiti,
|
||||
))
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
|
@ -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::<Vec<_>>();
|
||||
@ -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
|
||||
|
@ -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<Graffiti>,
|
||||
}
|
||||
|
||||
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<Graffiti> {
|
||||
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 {
|
||||
|
@ -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<T: EthSpec> ProductionValidatorClient<T> {
|
||||
.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()
|
||||
|
@ -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<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
voting_keystore_path: P,
|
||||
password: ZeroizeString,
|
||||
enable: bool,
|
||||
graffiti: Option<GraffitiString>,
|
||||
) -> Result<ValidatorDefinition, String> {
|
||||
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<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(
|
||||
&self,
|
||||
validator_pubkey: &PublicKey,
|
||||
|
Loading…
Reference in New Issue
Block a user