Skip to content

Commit

Permalink
Add /eth/v1/validator/{pubkey}/voluntary_exit endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Tumas committed Mar 28, 2024
1 parent badb5b3 commit b39ce29
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 21 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions runtime/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ pub async fn run_after_genesis<P: Preset>(
attestation_agg_pool.clone_arc(),
builder_api,
keymanager.proposer_configs().clone_arc(),
signer,
signer.clone_arc(),
slashing_protector,
sync_committee_agg_pool.clone_arc(),
bls_to_execution_change_pool.clone_arc(),
Expand Down Expand Up @@ -558,7 +558,7 @@ pub async fn run_after_genesis<P: Preset>(
let run_metrics_server = match metrics_server_config {
Some(config) => Either::Left(run_metrics_server(
config,
controller,
controller.clone_arc(),
registry.take(),
metrics.expect("Metrics registry must be present for metrics server"),
metrics_to_metrics_tx,
Expand All @@ -580,8 +580,10 @@ pub async fn run_after_genesis<P: Preset>(
let run_validator_api = match validator_api_config {
Some(validator_api_config) => Either::Left(run_validator_api(
validator_api_config,
keymanager,
controller,
directories,
keymanager,
signer,
)),
None => Either::Right(core::future::pending()),
};
Expand Down
3 changes: 2 additions & 1 deletion signer/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use types::{
phase0::{
containers::{
AggregateAndProof, AttestationData, BeaconBlock as Phase0BeaconBlock,
BeaconBlockHeader, Fork,
BeaconBlockHeader, Fork, VoluntaryExit,
},
primitives::{Epoch, Slot, H256},
},
Expand Down Expand Up @@ -73,6 +73,7 @@ pub enum SigningMessage<'block, P: Preset> {
SyncAggregatorSelectionData(SyncAggregatorSelectionData),
ContributionAndProof(ContributionAndProof<P>),
ValidatorRegistration(ValidatorRegistrationV1),
VoluntaryExit(VoluntaryExit),
}

impl<'block, P: Preset> From<&'block Phase0BeaconBlock<P>> for SigningMessage<'block, P> {
Expand Down
3 changes: 3 additions & 0 deletions signer/src/web3signer/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ impl<'block, P: Preset> SigningRequest<'block, P> {
MessageType::SyncCommitteeContributionAndProof
}
SigningMessage::ValidatorRegistration(_) => MessageType::ValidatorRegistration,
SigningMessage::VoluntaryExit(_) => MessageType::VoluntaryExit,
};

Self {
Expand All @@ -62,6 +63,7 @@ enum MessageType {
SyncCommitteeSelectionProof,
SyncCommitteeContributionAndProof,
ValidatorRegistration,
VoluntaryExit,
}

#[derive(Debug, Deserialize)]
Expand All @@ -88,6 +90,7 @@ mod tests {
"SYNC_COMMITTEE_SELECTION_PROOF",
"SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF",
"VALIDATOR_REGISTRATION",
"VOLUNTARY_EXIT",
],
);
}
Expand Down
1 change: 1 addition & 0 deletions validator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fs-err = { workspace = true }
futures = { workspace = true }
helper_functions = { workspace = true }
hex = { workspace = true }
http_api_utils = { workspace = true }
itertools = { workspace = true }
jwt-simple = { workspace = true }
keymanager = { workspace = true }
Expand Down
148 changes: 131 additions & 17 deletions validator/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use anyhow::{Error as AnyhowError, Result};
use axum::{
async_trait,
body::Body,
extract::{FromRef, FromRequest, FromRequestParts, Path as RequestPath, State},
extract::{FromRef, FromRequest, FromRequestParts, Path as RequestPath, Query, State},
headers::{authorization::Bearer, Authorization},
http::{request::Parts, Request, StatusCode},
middleware::Next,
Expand All @@ -20,18 +20,30 @@ use axum::{
use bls::PublicKeyBytes;
use directories::Directories;
use educe::Educe;
use eth1_api::ApiController;
use fork_choice_control::Wait;
use helper_functions::{accessors, signing::SignForSingleFork};
use jwt_simple::{
algorithms::{HS256Key, MACLike as _},
claims::{JWTClaims, NoCustomClaims},
reexports::coarsetime::Clock,
};
use keymanager::{KeyManager, KeymanagerOperationStatus, RemoteKey, ValidatingPubkey};
use log::{debug, info};
use serde::{Deserialize, Serialize};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use signer::{Signer, SigningMessage};
use std_ext::ArcExt as _;
use thiserror::Error;
use tokio::sync::RwLock;
use tower_http::cors::AllowOrigin;
use types::{bellatrix::primitives::Gas, phase0::primitives::ExecutionAddress};
use types::{
bellatrix::primitives::Gas,
phase0::{
containers::{SignedVoluntaryExit, VoluntaryExit},
primitives::{Epoch, ExecutionAddress},
},
preset::Preset,
};
use zeroize::Zeroizing;

const VALIDATOR_API_TOKEN_PATH: &str = "api-token.txt";
Expand Down Expand Up @@ -71,14 +83,25 @@ enum Error {
InvalidJsonBody(#[source] AnyhowError),
#[error("invalid public key")]
InvalidPublicKey(#[source] AnyhowError),
#[error("invalid query string")]
InvalidQuery(#[source] AnyhowError),
#[error("authentication error")]
Unauthorized(#[source] AnyhowError),
#[error("validator {pubkey} not found")]
ValidatorNotFound { pubkey: PublicKeyBytes },
#[error("validator {pubkey} is not managed by validator client")]
ValidatorNotOwned { pubkey: PublicKeyBytes },
}

impl IntoResponse for Error {
fn into_response(self) -> Response {
match self {
Self::InvalidJsonBody(_) | Self::InvalidPublicKey(_) => StatusCode::BAD_REQUEST,
Self::InvalidJsonBody(_) | Self::InvalidPublicKey(_) | Self::InvalidQuery(_) => {
StatusCode::BAD_REQUEST
}
Self::ValidatorNotFound { pubkey: _ } | Self::ValidatorNotOwned { pubkey: _ } => {
StatusCode::NOT_FOUND
}
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
}
Expand Down Expand Up @@ -252,24 +275,54 @@ impl<S> FromRequestParts<S> for EthPath<PublicKeyBytes> {
}
}

struct EthQuery<T>(pub T);

#[async_trait]
impl<S, T: DeserializeOwned + 'static> FromRequestParts<S> for EthQuery<T> {
type Rejection = Error;

async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
parts
.extract()
.await
.map(|Query(query)| Self(query))
.map_err(AnyhowError::msg)
.map_err(Error::InvalidQuery)
}
}

#[derive(Clone)]
struct ValidatorApiState {
struct ValidatorApiState<P: Preset, W: Wait> {
controller: ApiController<P, W>,
keymanager: Arc<KeyManager>,
secret: Arc<Secret>,
signer: Arc<RwLock<Signer>>,
}

impl<P: Preset, W: Wait> FromRef<ValidatorApiState<P, W>> for ApiController<P, W> {
fn from_ref(state: &ValidatorApiState<P, W>) -> Self {
state.controller.clone_arc()
}
}

impl FromRef<ValidatorApiState> for Arc<KeyManager> {
fn from_ref(state: &ValidatorApiState) -> Self {
impl<P: Preset, W: Wait> FromRef<ValidatorApiState<P, W>> for Arc<KeyManager> {
fn from_ref(state: &ValidatorApiState<P, W>) -> Self {
state.keymanager.clone_arc()
}
}

impl FromRef<ValidatorApiState> for Arc<Secret> {
fn from_ref(state: &ValidatorApiState) -> Self {
impl<P: Preset, W: Wait> FromRef<ValidatorApiState<P, W>> for Arc<Secret> {
fn from_ref(state: &ValidatorApiState<P, W>) -> Self {
state.secret.clone_arc()
}
}

impl<P: Preset, W: Wait> FromRef<ValidatorApiState<P, W>> for Arc<RwLock<Signer>> {
fn from_ref(state: &ValidatorApiState<P, W>) -> Self {
state.signer.clone_arc()
}
}

#[derive(Deserialize)]
struct SetFeeRecipientQuery {
ethaddress: ExecutionAddress,
Expand Down Expand Up @@ -319,6 +372,11 @@ struct ProposerConfigResponse {
graffiti: Option<String>,
}

#[derive(Deserialize)]
struct CreateVoluntaryExitQuery {
epoch: Option<Epoch>,
}

/// `GET /eth/v1/validator/{pubkey}/feerecipient`
async fn keymanager_list_fee_recipient(
State(keymanager): State<Arc<KeyManager>>,
Expand Down Expand Up @@ -518,6 +576,48 @@ async fn keymanager_delete_remote_keys(
Ok(EthResponse::json(delete_statuses))
}

/// `POST /eth/v1/validator/{pubkey}/voluntary_exit`
async fn keymanager_create_voluntary_exit<P: Preset, W: Wait>(
State(controller): State<ApiController<P, W>>,
State(signer): State<Arc<RwLock<Signer>>>,
EthPath(pubkey): EthPath<PublicKeyBytes>,
EthQuery(query): EthQuery<CreateVoluntaryExitQuery>,
) -> Result<EthResponse<SignedVoluntaryExit>, Error> {
let state = controller.preprocessed_state_at_current_slot()?;

let epoch = query
.epoch
.unwrap_or_else(|| accessors::get_current_epoch(&state));

if !signer.read().await.has_key(pubkey) {
return Err(Error::ValidatorNotOwned { pubkey });
}

let validator_index = accessors::index_of_public_key(&state, pubkey)
.ok_or(Error::ValidatorNotFound { pubkey })?;

let voluntary_exit = VoluntaryExit {
epoch,
validator_index,
};

let signature = signer
.read()
.await
.sign(
SigningMessage::VoluntaryExit(voluntary_exit),
voluntary_exit.signing_root(controller.chain_config(), &state),
Some(state.as_ref().into()),
pubkey,
)
.await?;

Ok(EthResponse::json(SignedVoluntaryExit {
message: voluntary_exit,
signature: signature.into(),
}))
}

async fn authorize_token(
State(secret): State<Arc<Secret>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Expand All @@ -534,21 +634,28 @@ async fn authorize_token(
}

#[allow(clippy::module_name_repetitions)]
pub async fn run_validator_api(
pub async fn run_validator_api<P: Preset, W: Wait>(
validator_api_config: ValidatorApiConfig,
keymanager: Arc<KeyManager>,
controller: ApiController<P, W>,
directories: Arc<Directories>,
keymanager: Arc<KeyManager>,
signer: Arc<RwLock<Signer>>,
) -> Result<()> {
let Auth { secret, token } = load_or_build_auth_token(&directories)?;

info!(
"Validator API is listening on {}, authorization token: {token}",
validator_api_config.address
);
let ValidatorApiConfig {
address,
allow_origin,
timeout,
} = validator_api_config;

info!("Validator API is listening on {address}, authorization token: {token}");

let state = ValidatorApiState {
controller,
keymanager,
secret: Arc::new(secret),
signer,
};

let router = eth_v1_keymanager_routes()
Expand All @@ -558,13 +665,16 @@ pub async fn run_validator_api(
))
.with_state(state);

Server::bind(&validator_api_config.address)
let router =
http_api_utils::extend_router_with_middleware(router, Some(timeout), allow_origin, None);

Server::bind(&address)
.serve(router.into_make_service_with_connect_info::<SocketAddr>())
.await
.map_err(AnyhowError::new)
}

fn eth_v1_keymanager_routes() -> Router<ValidatorApiState> {
fn eth_v1_keymanager_routes<P: Preset, W: Wait>() -> Router<ValidatorApiState<P, W>> {
Router::new()
.route(
"/eth/v1/validator/:pubkey/feerecipient",
Expand Down Expand Up @@ -602,6 +712,10 @@ fn eth_v1_keymanager_routes() -> Router<ValidatorApiState> {
"/eth/v1/validator/:pubkey/graffiti",
delete(keymanager_delete_graffiti),
)
.route(
"/eth/v1/validator/:pubkey/voluntary_exit",
post(keymanager_create_voluntary_exit),
)
.route("/eth/v1/keystores", get(keymanager_list_validating_pubkeys))
.route("/eth/v1/keystores", post(keymanager_import_keystores))
.route("/eth/v1/keystores", delete(keymanager_delete_keystores))
Expand Down

0 comments on commit b39ce29

Please sign in to comment.