Add support for updating validator graffiti (#4417)

## Issue Addressed

#4386 

## Proposed Changes

The original proposal described in the issue adds a new endpoint to support updating validator graffiti, but I realized we already have an endpoint that we use for updating various validator fields in memory and in the validator definitions file, so I think that would be the best place to add this to.

### API endpoint

`PATCH lighthouse/validators/{validator_pubkey}` 

This endpoint updates the graffiti in both the [ validator definition file](https://lighthouse-book.sigmaprime.io/graffiti.html#2-setting-the-graffiti-in-the-validator_definitionsyml) and the in memory `InitializedValidators`. In the next block proposal, the new graffiti will be used.

Note that the [`--graffiti-file`](https://lighthouse-book.sigmaprime.io/graffiti.html#1-using-the---graffiti-file-flag-on-the-validator-client) flag has a priority over the validator definitions file, so if the caller attempts to update the graffiti while the `--graffiti-file` flag is present, the endpoint will return an error (Bad request 400).

## Tasks

- [x] Add graffiti update support to `PATCH lighthouse/validators/{validator_pubkey}` 
- [x] Return error if user tries to update graffiti while the `--graffiti-flag` is set
- [x] Update Lighthouse book
This commit is contained in:
Jimmy Chen 2023-06-22 02:14:57 +00:00
parent 3cac6d9ed5
commit 33c942ff03
8 changed files with 143 additions and 11 deletions

View File

@ -426,7 +426,8 @@ Example Response Body
## `PATCH /lighthouse/validators/:voting_pubkey` ## `PATCH /lighthouse/validators/:voting_pubkey`
Update some values for the validator with `voting_pubkey`. The following example updates a validator from `enabled: true` to `enabled: false` Update some values for the validator with `voting_pubkey`. Possible fields: `enabled`, `gas_limit`, `builder_proposals`,
and `graffiti`. The following example updates a validator from `enabled: true` to `enabled: false`.
### HTTP Specification ### HTTP Specification

View File

@ -29,6 +29,8 @@ Lighthouse will first search for the graffiti corresponding to the public key of
### 2. Setting the graffiti in the `validator_definitions.yml` ### 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. 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.
You can also update the graffitis in the `validator_definitions.yml` file using the [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey). See example in [Set Graffiti via HTTP](#set-graffiti-via-http).
Below is an example of the validator_definitions.yml with validator specific graffitis: Below is an example of the validator_definitions.yml with validator specific graffitis:
``` ```
--- ---
@ -62,3 +64,25 @@ Usage: `lighthouse bn --graffiti fortytwo`
> 3. If graffiti is not specified in `validator_definitions.yml`, load the graffiti passed in the `--graffiti` flag on the validator client. > 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 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. > 4. If the `--graffiti` flag is not passed, load the default Lighthouse graffiti.
### Set Graffiti via HTTP
Use the [Lighthouse API](api-vc-endpoints.md) to set graffiti on a per-validator basis. This method updates the graffiti
both in memory and in the `validator_definitions.yml` file. The new graffiti will be used in the next block proposal
without requiring a validator client restart.
Refer to [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey) for API specification.
#### Example Command
```bash
DATADIR=/var/lib/lighthouse
curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \
-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \
-H "Content-Type: application/json" \
-d '{
"graffiti": "Mr F was here"
}' | jq
```
A `null` response indicates that the request is successful.

View File

@ -16,6 +16,7 @@ use std::path::Path;
pub use reqwest; pub use reqwest;
pub use reqwest::{Response, StatusCode, Url}; pub use reqwest::{Response, StatusCode, Url};
use types::graffiti::GraffitiString;
/// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a
/// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`). /// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`).
@ -467,6 +468,7 @@ impl ValidatorClientHttpClient {
enabled: Option<bool>, enabled: Option<bool>,
gas_limit: Option<u64>, gas_limit: Option<u64>,
builder_proposals: Option<bool>, builder_proposals: Option<bool>,
graffiti: Option<GraffitiString>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut path = self.server.full.clone(); let mut path = self.server.full.clone();
@ -482,6 +484,7 @@ impl ValidatorClientHttpClient {
enabled, enabled,
gas_limit, gas_limit,
builder_proposals, builder_proposals,
graffiti,
}, },
) )
.await .await

View File

@ -83,6 +83,9 @@ pub struct ValidatorPatchRequest {
#[serde(default)] #[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub builder_proposals: Option<bool>, pub builder_proposals: Option<bool>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub graffiti: Option<GraffitiString>,
} }
#[derive(Clone, PartialEq, Serialize, Deserialize)] #[derive(Clone, PartialEq, Serialize, Deserialize)]

View File

@ -357,7 +357,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.and(warp::path("graffiti")) .and(warp::path("graffiti"))
.and(warp::path::end()) .and(warp::path::end())
.and(validator_store_filter.clone()) .and(validator_store_filter.clone())
.and(graffiti_file_filter) .and(graffiti_file_filter.clone())
.and(graffiti_flag_filter) .and(graffiti_flag_filter)
.and(signer.clone()) .and(signer.clone())
.and(log_filter.clone()) .and(log_filter.clone())
@ -617,18 +617,27 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.and(warp::path::end()) .and(warp::path::end())
.and(warp::body::json()) .and(warp::body::json())
.and(validator_store_filter.clone()) .and(validator_store_filter.clone())
.and(graffiti_file_filter)
.and(signer.clone()) .and(signer.clone())
.and(task_executor_filter.clone()) .and(task_executor_filter.clone())
.and_then( .and_then(
|validator_pubkey: PublicKey, |validator_pubkey: PublicKey,
body: api_types::ValidatorPatchRequest, body: api_types::ValidatorPatchRequest,
validator_store: Arc<ValidatorStore<T, E>>, validator_store: Arc<ValidatorStore<T, E>>,
graffiti_file: Option<GraffitiFile>,
signer, signer,
task_executor: TaskExecutor| { task_executor: TaskExecutor| {
blocking_signed_json_task(signer, move || { blocking_signed_json_task(signer, move || {
if body.graffiti.is_some() && graffiti_file.is_some() {
return Err(warp_utils::reject::custom_bad_request(
"Unable to update graffiti as the \"--graffiti-file\" flag is set"
.to_string(),
));
}
let maybe_graffiti = body.graffiti.clone().map(Into::into);
let initialized_validators_rw_lock = validator_store.initialized_validators(); let initialized_validators_rw_lock = validator_store.initialized_validators();
let mut initialized_validators = initialized_validators_rw_lock.write(); let mut initialized_validators = initialized_validators_rw_lock.write();
match ( match (
initialized_validators.is_enabled(&validator_pubkey), initialized_validators.is_enabled(&validator_pubkey),
initialized_validators.validator(&validator_pubkey.compress()), initialized_validators.validator(&validator_pubkey.compress()),
@ -641,7 +650,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
if Some(is_enabled) == body.enabled if Some(is_enabled) == body.enabled
&& initialized_validator.get_gas_limit() == body.gas_limit && initialized_validator.get_gas_limit() == body.gas_limit
&& initialized_validator.get_builder_proposals() && initialized_validator.get_builder_proposals()
== body.builder_proposals => == body.builder_proposals
&& initialized_validator.get_graffiti() == maybe_graffiti =>
{ {
Ok(()) Ok(())
} }
@ -654,6 +664,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
body.enabled, body.enabled,
body.gas_limit, body.gas_limit,
body.builder_proposals, body.builder_proposals,
body.graffiti,
), ),
) )
.map_err(|e| { .map_err(|e| {

View File

@ -28,12 +28,14 @@ use slot_clock::{SlotClock, TestingSlotClock};
use std::future::Future; use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::net::{IpAddr, Ipv4Addr}; use std::net::{IpAddr, Ipv4Addr};
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use task_executor::TaskExecutor; use task_executor::TaskExecutor;
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use types::graffiti::GraffitiString;
const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; const PASSWORD_BYTES: &[u8] = &[42, 50, 37];
pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42);
@ -533,7 +535,7 @@ impl ApiTester {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
self.client self.client
.patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None) .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None, None)
.await .await
.unwrap(); .unwrap();
@ -575,7 +577,13 @@ impl ApiTester {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
self.client self.client
.patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None) .patch_lighthouse_validators(
&validator.voting_pubkey,
None,
Some(gas_limit),
None,
None,
)
.await .await
.unwrap(); .unwrap();
@ -602,6 +610,7 @@ impl ApiTester {
None, None,
None, None,
Some(builder_proposals), Some(builder_proposals),
None,
) )
.await .await
.unwrap(); .unwrap();
@ -620,6 +629,34 @@ impl ApiTester {
self self
} }
pub async fn set_graffiti(self, index: usize, graffiti: &str) -> Self {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
let graffiti_str = GraffitiString::from_str(graffiti).unwrap();
self.client
.patch_lighthouse_validators(
&validator.voting_pubkey,
None,
None,
None,
Some(graffiti_str),
)
.await
.unwrap();
self
}
pub async fn assert_graffiti(self, index: usize, graffiti: &str) -> Self {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
let graffiti_str = GraffitiString::from_str(graffiti).unwrap();
assert_eq!(
self.validator_store.graffiti(&validator.voting_pubkey),
Some(graffiti_str.into())
);
self
}
} }
struct HdValidatorScenario { struct HdValidatorScenario {
@ -723,7 +760,13 @@ fn routes_with_invalid_auth() {
.await .await
.test_with_invalid_auth(|client| async move { .test_with_invalid_auth(|client| async move {
client client
.patch_lighthouse_validators(&PublicKeyBytes::empty(), Some(false), None, None) .patch_lighthouse_validators(
&PublicKeyBytes::empty(),
Some(false),
None,
None,
None,
)
.await .await
}) })
.await .await
@ -931,6 +974,41 @@ fn validator_builder_proposals() {
}); });
} }
#[test]
fn validator_graffiti() {
let runtime = build_runtime();
let weak_runtime = Arc::downgrade(&runtime);
runtime.block_on(async {
ApiTester::new(weak_runtime)
.await
.create_hd_validators(HdValidatorScenario {
count: 2,
specify_mnemonic: false,
key_derivation_path_offset: 0,
disabled: vec![],
})
.await
.assert_enabled_validators_count(2)
.assert_validators_count(2)
.set_graffiti(0, "Mr F was here")
.await
.assert_graffiti(0, "Mr F was here")
.await
// Test setting graffiti while the validator is disabled
.set_validator_enabled(0, false)
.await
.assert_enabled_validators_count(1)
.assert_validators_count(2)
.set_graffiti(0, "Mr F was here again")
.await
.set_validator_enabled(0, true)
.await
.assert_enabled_validators_count(2)
.assert_graffiti(0, "Mr F was here again")
.await
});
}
#[test] #[test]
fn keystore_validator_creation() { fn keystore_validator_creation() {
let runtime = build_runtime(); let runtime = build_runtime();

View File

@ -468,7 +468,7 @@ fn import_and_delete_conflicting_web3_signer_keystores() {
for pubkey in &pubkeys { for pubkey in &pubkeys {
tester tester
.client .client
.patch_lighthouse_validators(pubkey, Some(false), None, None) .patch_lighthouse_validators(pubkey, Some(false), None, None, None)
.await .await
.unwrap(); .unwrap();
} }

View File

@ -27,6 +27,7 @@ use std::io::{self, Read};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use types::graffiti::GraffitiString;
use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes};
use url::{ParseError, Url}; use url::{ParseError, Url};
use validator_dir::Builder as ValidatorDirBuilder; use validator_dir::Builder as ValidatorDirBuilder;
@ -147,6 +148,10 @@ impl InitializedValidator {
pub fn get_index(&self) -> Option<u64> { pub fn get_index(&self) -> Option<u64> {
self.index self.index
} }
pub fn get_graffiti(&self) -> Option<Graffiti> {
self.graffiti
}
} }
fn open_keystore(path: &Path) -> Result<Keystore, Error> { fn open_keystore(path: &Path) -> Result<Keystore, Error> {
@ -671,8 +676,8 @@ impl InitializedValidators {
self.validators.get(public_key) self.validators.get(public_key)
} }
/// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, and `builder_proposals` /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`,
/// values. /// `builder_proposals`, and `graffiti` values.
/// ///
/// ## Notes /// ## Notes
/// ///
@ -682,7 +687,7 @@ impl InitializedValidators {
/// ///
/// If a `gas_limit` is included in the call to this function, it will also be updated and saved /// If a `gas_limit` is included in the call to this function, it will also be updated and saved
/// to disk. If `gas_limit` is `None` the `gas_limit` *will not* be unset in `ValidatorDefinition` /// to disk. If `gas_limit` is `None` the `gas_limit` *will not* be unset in `ValidatorDefinition`
/// or `InitializedValidator`. The same logic applies to `builder_proposals`. /// or `InitializedValidator`. The same logic applies to `builder_proposals` and `graffiti`.
/// ///
/// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed.
pub async fn set_validator_definition_fields( pub async fn set_validator_definition_fields(
@ -691,6 +696,7 @@ impl InitializedValidators {
enabled: Option<bool>, enabled: Option<bool>,
gas_limit: Option<u64>, gas_limit: Option<u64>,
builder_proposals: Option<bool>, builder_proposals: Option<bool>,
graffiti: Option<GraffitiString>,
) -> Result<(), Error> { ) -> Result<(), Error> {
if let Some(def) = self if let Some(def) = self
.definitions .definitions
@ -708,6 +714,9 @@ impl InitializedValidators {
if let Some(builder_proposals) = builder_proposals { if let Some(builder_proposals) = builder_proposals {
def.builder_proposals = Some(builder_proposals); def.builder_proposals = Some(builder_proposals);
} }
if let Some(graffiti) = graffiti.clone() {
def.graffiti = Some(graffiti);
}
} }
self.update_validators().await?; self.update_validators().await?;
@ -723,6 +732,9 @@ impl InitializedValidators {
if let Some(builder_proposals) = builder_proposals { if let Some(builder_proposals) = builder_proposals {
val.builder_proposals = Some(builder_proposals); val.builder_proposals = Some(builder_proposals);
} }
if let Some(graffiti) = graffiti {
val.graffiti = Some(graffiti.into());
}
} }
self.definitions self.definitions