mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-02 16:21:42 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user