Skip to content

Commit

Permalink
Implement graffiti management API (#4951)
Browse files Browse the repository at this point in the history
* implement get graffiti

* add set graffiti

* add set graffiti

* delete graffiti

* set graffiti

* set graffiti

* fmt

* added tests

* add graffiti file check

* update

* fixed delete req

* remove unused code

* changes based on feedback

* changes based on feedback

* invalid auth test plus lint

* fmt

* remove unneeded async
  • Loading branch information
eserilev authored Dec 7, 2023
1 parent d9d8424 commit 8ba39cb
Show file tree
Hide file tree
Showing 7 changed files with 394 additions and 3 deletions.
61 changes: 61 additions & 0 deletions common/eth2/src/lighthouse_vc/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,32 @@ impl ValidatorClientHttpClient {
ok_or_error(response).await
}

/// Perform a HTTP DELETE request, returning the `Response` for further processing.
async fn delete_response<U: IntoUrl>(&self, url: U) -> Result<Response, Error> {
let response = self
.client
.delete(url)
.headers(self.headers()?)
.send()
.await
.map_err(Error::from)?;
ok_or_error(response).await
}

async fn get<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<T, Error> {
let response = self.get_response(url).await?;
self.signed_json(response).await
}

async fn delete<U: IntoUrl>(&self, url: U) -> Result<(), Error> {
let response = self.delete_response(url).await?;
if response.status().is_success() {
Ok(())
} else {
Err(Error::StatusCode(response.status()))
}
}

async fn get_unsigned<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<T, Error> {
self.get_response(url)
.await?
Expand Down Expand Up @@ -537,6 +558,18 @@ impl ValidatorClientHttpClient {
Ok(url)
}

fn make_graffiti_url(&self, pubkey: &PublicKeyBytes) -> Result<Url, Error> {
let mut url = self.server.full.clone();
url.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
.push("eth")
.push("v1")
.push("validator")
.push(&pubkey.to_string())
.push("graffiti");
Ok(url)
}

fn make_gas_limit_url(&self, pubkey: &PublicKeyBytes) -> Result<Url, Error> {
let mut url = self.server.full.clone();
url.path_segments_mut()
Expand Down Expand Up @@ -684,6 +717,34 @@ impl ValidatorClientHttpClient {

self.post(path, &()).await
}

/// `GET /eth/v1/validator/{pubkey}/graffiti`
pub async fn get_graffiti(
&self,
pubkey: &PublicKeyBytes,
) -> Result<GetGraffitiResponse, Error> {
let url = self.make_graffiti_url(pubkey)?;
self.get(url)
.await
.map(|generic: GenericResponse<GetGraffitiResponse>| generic.data)
}

/// `POST /eth/v1/validator/{pubkey}/graffiti`
pub async fn set_graffiti(
&self,
pubkey: &PublicKeyBytes,
graffiti: GraffitiString,
) -> Result<(), Error> {
let url = self.make_graffiti_url(pubkey)?;
let set_graffiti_request = SetGraffitiRequest { graffiti };
self.post(url, &set_graffiti_request).await
}

/// `DELETE /eth/v1/validator/{pubkey}/graffiti`
pub async fn delete_graffiti(&self, pubkey: &PublicKeyBytes) -> Result<(), Error> {
let url = self.make_graffiti_url(pubkey)?;
self.delete(url).await
}
}

/// Returns `Ok(response)` if the response is a `200 OK` response or a
Expand Down
8 changes: 7 additions & 1 deletion common/eth2/src/lighthouse_vc/std_types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use account_utils::ZeroizeString;
use eth2_keystore::Keystore;
use serde::{Deserialize, Serialize};
use types::{Address, PublicKeyBytes};
use types::{Address, Graffiti, PublicKeyBytes};

pub use slashing_protection::interchange::Interchange;

Expand Down Expand Up @@ -172,3 +172,9 @@ pub enum DeleteRemotekeyStatus {
pub struct DeleteRemotekeysResponse {
pub data: Vec<Status<DeleteRemotekeyStatus>>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct GetGraffitiResponse {
pub pubkey: PublicKeyBytes,
pub graffiti: Graffiti,
}
5 changes: 5 additions & 0 deletions common/eth2/src/lighthouse_vc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,8 @@ pub struct SingleExportKeystoresResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub validating_keystore_password: Option<ZeroizeString>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct SetGraffitiRequest {
pub graffiti: GraffitiString,
}
80 changes: 80 additions & 0 deletions validator_client/src/http_api/graffiti.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::validator_store::ValidatorStore;
use bls::PublicKey;
use slot_clock::SlotClock;
use std::sync::Arc;
use types::{graffiti::GraffitiString, EthSpec, Graffiti};

pub fn get_graffiti<T: 'static + SlotClock + Clone, E: EthSpec>(
validator_pubkey: PublicKey,
validator_store: Arc<ValidatorStore<T, E>>,
graffiti_flag: Option<Graffiti>,
) -> Result<Graffiti, warp::Rejection> {
let initialized_validators_rw_lock = validator_store.initialized_validators();
let initialized_validators = initialized_validators_rw_lock.read();
match initialized_validators.validator(&validator_pubkey.compress()) {
None => Err(warp_utils::reject::custom_not_found(
"The key was not found on the server".to_string(),
)),
Some(_) => {
let Some(graffiti) = initialized_validators.graffiti(&validator_pubkey.into()) else {
return graffiti_flag.ok_or(warp_utils::reject::custom_server_error(
"No graffiti found, unable to return the process-wide default".to_string(),
));
};
Ok(graffiti)
}
}
}

pub fn set_graffiti<T: 'static + SlotClock + Clone, E: EthSpec>(
validator_pubkey: PublicKey,
graffiti: GraffitiString,
validator_store: Arc<ValidatorStore<T, E>>,
) -> Result<(), warp::Rejection> {
let initialized_validators_rw_lock = validator_store.initialized_validators();
let mut initialized_validators = initialized_validators_rw_lock.write();
match initialized_validators.validator(&validator_pubkey.compress()) {
None => Err(warp_utils::reject::custom_not_found(
"The key was not found on the server, nothing to update".to_string(),
)),
Some(initialized_validator) => {
if initialized_validator.get_graffiti() == Some(graffiti.clone().into()) {
Ok(())
} else {
initialized_validators
.set_graffiti(&validator_pubkey, graffiti)
.map_err(|_| {
warp_utils::reject::custom_server_error(
"A graffiti was found, but failed to be updated.".to_string(),
)
})
}
}
}
}

pub fn delete_graffiti<T: 'static + SlotClock + Clone, E: EthSpec>(
validator_pubkey: PublicKey,
validator_store: Arc<ValidatorStore<T, E>>,
) -> Result<(), warp::Rejection> {
let initialized_validators_rw_lock = validator_store.initialized_validators();
let mut initialized_validators = initialized_validators_rw_lock.write();
match initialized_validators.validator(&validator_pubkey.compress()) {
None => Err(warp_utils::reject::custom_not_found(
"The key was not found on the server, nothing to delete".to_string(),
)),
Some(initialized_validator) => {
if initialized_validator.get_graffiti().is_none() {
Ok(())
} else {
initialized_validators
.delete_graffiti(&validator_pubkey)
.map_err(|_| {
warp_utils::reject::custom_server_error(
"A graffiti was found, but failed to be removed.".to_string(),
)
})
}
}
}
}
93 changes: 91 additions & 2 deletions validator_client/src/http_api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
mod api_secret;
mod create_signed_voluntary_exit;
mod create_validator;
mod graffiti;
mod keystores;
mod remotekeys;
mod tests;

pub mod test_utils;

use crate::http_api::graffiti::{delete_graffiti, get_graffiti, set_graffiti};

use crate::http_api::create_signed_voluntary_exit::create_signed_voluntary_exit;
use crate::{determine_graffiti, GraffitiFile, ValidatorStore};
use account_utils::{
Expand All @@ -19,7 +22,10 @@ use create_validator::{
};
use eth2::lighthouse_vc::{
std_types::{AuthResponse, GetFeeRecipientResponse, GetGasLimitResponse},
types::{self as api_types, GenericResponse, Graffiti, PublicKey, PublicKeyBytes},
types::{
self as api_types, GenericResponse, GetGraffitiResponse, Graffiti, PublicKey,
PublicKeyBytes, SetGraffitiRequest,
},
};
use lighthouse_version::version_with_platform;
use logging::SSELoggingComponents;
Expand Down Expand Up @@ -653,7 +659,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.and(warp::path::end())
.and(warp::body::json())
.and(validator_store_filter.clone())
.and(graffiti_file_filter)
.and(graffiti_file_filter.clone())
.and(signer.clone())
.and(task_executor_filter.clone())
.and_then(
Expand Down Expand Up @@ -1028,6 +1034,86 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
},
);

// GET /eth/v1/validator/{pubkey}/graffiti
let get_graffiti = eth_v1
.and(warp::path("validator"))
.and(warp::path::param::<PublicKey>())
.and(warp::path("graffiti"))
.and(warp::path::end())
.and(validator_store_filter.clone())
.and(graffiti_flag_filter)
.and(signer.clone())
.and_then(
|pubkey: PublicKey,
validator_store: Arc<ValidatorStore<T, E>>,
graffiti_flag: Option<Graffiti>,
signer| {
blocking_signed_json_task(signer, move || {
let graffiti = get_graffiti(pubkey.clone(), validator_store, graffiti_flag)?;
Ok(GenericResponse::from(GetGraffitiResponse {
pubkey: pubkey.into(),
graffiti,
}))
})
},
);

// POST /eth/v1/validator/{pubkey}/graffiti
let post_graffiti = eth_v1
.and(warp::path("validator"))
.and(warp::path::param::<PublicKey>())
.and(warp::path("graffiti"))
.and(warp::body::json())
.and(warp::path::end())
.and(validator_store_filter.clone())
.and(graffiti_file_filter.clone())
.and(signer.clone())
.and_then(
|pubkey: PublicKey,
query: SetGraffitiRequest,
validator_store: Arc<ValidatorStore<T, E>>,
graffiti_file: Option<GraffitiFile>,
signer| {
blocking_signed_json_task(signer, move || {
if graffiti_file.is_some() {
return Err(warp_utils::reject::invalid_auth(
"Unable to update graffiti as the \"--graffiti-file\" flag is set"
.to_string(),
));
}
set_graffiti(pubkey.clone(), query.graffiti, validator_store)
})
},
)
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::ACCEPTED));

// DELETE /eth/v1/validator/{pubkey}/graffiti
let delete_graffiti = eth_v1
.and(warp::path("validator"))
.and(warp::path::param::<PublicKey>())
.and(warp::path("graffiti"))
.and(warp::path::end())
.and(validator_store_filter.clone())
.and(graffiti_file_filter.clone())
.and(signer.clone())
.and_then(
|pubkey: PublicKey,
validator_store: Arc<ValidatorStore<T, E>>,
graffiti_file: Option<GraffitiFile>,
signer| {
blocking_signed_json_task(signer, move || {
if graffiti_file.is_some() {
return Err(warp_utils::reject::invalid_auth(
"Unable to delete graffiti as the \"--graffiti-file\" flag is set"
.to_string(),
));
}
delete_graffiti(pubkey.clone(), validator_store)
})
},
)
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NO_CONTENT));

// GET /eth/v1/keystores
let get_std_keystores = std_keystores
.and(signer.clone())
Expand Down Expand Up @@ -1175,6 +1261,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.or(get_lighthouse_ui_graffiti)
.or(get_fee_recipient)
.or(get_gas_limit)
.or(get_graffiti)
.or(get_std_keystores)
.or(get_std_remotekeys)
.recover(warp_utils::reject::handle_rejection),
Expand All @@ -1189,6 +1276,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.or(post_gas_limit)
.or(post_std_keystores)
.or(post_std_remotekeys)
.or(post_graffiti)
.recover(warp_utils::reject::handle_rejection),
))
.or(warp::patch()
Expand All @@ -1199,6 +1287,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.or(delete_gas_limit)
.or(delete_std_keystores)
.or(delete_std_remotekeys)
.or(delete_graffiti)
.recover(warp_utils::reject::handle_rejection),
)),
)
Expand Down
Loading

0 comments on commit 8ba39cb

Please sign in to comment.