Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement graffiti management API #4951

Merged
merged 17 commits into from
Dec 7, 2023
65 changes: 65 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,38 @@ 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: Some(graffiti),
};
self.post(url, &set_graffiti_request).await?;
Ok(())
jimmygchen marked this conversation as resolved.
Show resolved Hide resolved
}

/// `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?;
Ok(())
eserilev marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// 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: Option<GraffitiString>,
jimmygchen marked this conversation as resolved.
Show resolved Hide resolved
}
81 changes: 81 additions & 0 deletions validator_client/src/http_api/graffiti.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use crate::validator_store::ValidatorStore;
use bls::PublicKey;
use slot_clock::SlotClock;
use std::sync::Arc;
use types::{graffiti::GraffitiString, EthSpec, Graffiti};

pub async fn get_graffiti<T: 'static + SlotClock + Clone, E: EthSpec>(
eserilev marked this conversation as resolved.
Show resolved Hide resolved
validator_pubkey: PublicKey,
validator_store: Arc<ValidatorStore<T, E>>,
) -> 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 Ok(Graffiti::default());
eserilev marked this conversation as resolved.
Show resolved Hide resolved
};
Ok(graffiti)
}
}
}

pub async 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(),
)
})?;

Ok(())
eserilev marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

pub async 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() == Some(Graffiti::default()) {
eserilev marked this conversation as resolved.
Show resolved Hide resolved
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(),
)
})?;

Ok(())
eserilev marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
123 changes: 121 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,116 @@ 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(signer.clone())
.and(task_executor_filter.clone())
.and_then(
|pubkey: PublicKey,
validator_store: Arc<ValidatorStore<T, E>>,
signer,
task_executor: TaskExecutor| {
blocking_signed_json_task(signer, move || {
if let Some(handle) = task_executor.handle() {
let graffiti =
handle.block_on(get_graffiti(pubkey.clone(), validator_store))?;
Ok(GenericResponse::from(GetGraffitiResponse {
pubkey: pubkey.into(),
graffiti,
}))
} else {
Err(warp_utils::reject::custom_server_error(
"An error occurred while attempting to get graffiti".into(),
))
}
})
},
);

// 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(task_executor_filter.clone())
.and_then(
|pubkey: PublicKey,
query: SetGraffitiRequest,
validator_store: Arc<ValidatorStore<T, E>>,
graffiti_file: Option<GraffitiFile>,
signer,
task_executor: TaskExecutor| {
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(),
));
}
if let Some(handle) = task_executor.handle() {
let Some(graffiti) = query.graffiti else {
return Err(warp_utils::reject::custom_bad_request(
"No graffiti provided".into(),
));
};
handle.block_on(set_graffiti(pubkey.clone(), graffiti, validator_store))?;
Ok(())
} else {
Err(warp_utils::reject::custom_server_error(
"An error occurred while attempting to set graffiti".into(),
))
}
})
},
)
.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(task_executor_filter.clone())
.and_then(
|pubkey: PublicKey,
validator_store: Arc<ValidatorStore<T, E>>,
graffiti_file: Option<GraffitiFile>,
signer,
task_executor: TaskExecutor| {
blocking_signed_json_task(signer, move || {
if graffiti_file.is_some() {
return Err(warp_utils::reject::invalid_auth(
jimmygchen marked this conversation as resolved.
Show resolved Hide resolved
"Unable to update graffiti as the \"--graffiti-file\" flag is set"
eserilev marked this conversation as resolved.
Show resolved Hide resolved
.to_string(),
));
}
if let Some(handle) = task_executor.handle() {
handle.block_on(delete_graffiti(pubkey.clone(), validator_store))?;
Ok(())
} else {
Err(warp_utils::reject::custom_server_error(
"An error occurred while attempting to delete graffiti".into(),
))
}
})
},
)
.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 +1291,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 +1306,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 +1317,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