Skip to content

Commit b39ce29

Browse files
committed
Add /eth/v1/validator/{pubkey}/voluntary_exit endpoint
1 parent badb5b3 commit b39ce29

File tree

6 files changed

+143
-21
lines changed

6 files changed

+143
-21
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

runtime/src/runtime.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ pub async fn run_after_genesis<P: Preset>(
473473
attestation_agg_pool.clone_arc(),
474474
builder_api,
475475
keymanager.proposer_configs().clone_arc(),
476-
signer,
476+
signer.clone_arc(),
477477
slashing_protector,
478478
sync_committee_agg_pool.clone_arc(),
479479
bls_to_execution_change_pool.clone_arc(),
@@ -558,7 +558,7 @@ pub async fn run_after_genesis<P: Preset>(
558558
let run_metrics_server = match metrics_server_config {
559559
Some(config) => Either::Left(run_metrics_server(
560560
config,
561-
controller,
561+
controller.clone_arc(),
562562
registry.take(),
563563
metrics.expect("Metrics registry must be present for metrics server"),
564564
metrics_to_metrics_tx,
@@ -580,8 +580,10 @@ pub async fn run_after_genesis<P: Preset>(
580580
let run_validator_api = match validator_api_config {
581581
Some(validator_api_config) => Either::Left(run_validator_api(
582582
validator_api_config,
583-
keymanager,
583+
controller,
584584
directories,
585+
keymanager,
586+
signer,
585587
)),
586588
None => Either::Right(core::future::pending()),
587589
};

signer/src/types.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use types::{
1818
phase0::{
1919
containers::{
2020
AggregateAndProof, AttestationData, BeaconBlock as Phase0BeaconBlock,
21-
BeaconBlockHeader, Fork,
21+
BeaconBlockHeader, Fork, VoluntaryExit,
2222
},
2323
primitives::{Epoch, Slot, H256},
2424
},
@@ -73,6 +73,7 @@ pub enum SigningMessage<'block, P: Preset> {
7373
SyncAggregatorSelectionData(SyncAggregatorSelectionData),
7474
ContributionAndProof(ContributionAndProof<P>),
7575
ValidatorRegistration(ValidatorRegistrationV1),
76+
VoluntaryExit(VoluntaryExit),
7677
}
7778

7879
impl<'block, P: Preset> From<&'block Phase0BeaconBlock<P>> for SigningMessage<'block, P> {

signer/src/web3signer/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ impl<'block, P: Preset> SigningRequest<'block, P> {
3838
MessageType::SyncCommitteeContributionAndProof
3939
}
4040
SigningMessage::ValidatorRegistration(_) => MessageType::ValidatorRegistration,
41+
SigningMessage::VoluntaryExit(_) => MessageType::VoluntaryExit,
4142
};
4243

4344
Self {
@@ -62,6 +63,7 @@ enum MessageType {
6263
SyncCommitteeSelectionProof,
6364
SyncCommitteeContributionAndProof,
6465
ValidatorRegistration,
66+
VoluntaryExit,
6567
}
6668

6769
#[derive(Debug, Deserialize)]
@@ -88,6 +90,7 @@ mod tests {
8890
"SYNC_COMMITTEE_SELECTION_PROOF",
8991
"SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF",
9092
"VALIDATOR_REGISTRATION",
93+
"VOLUNTARY_EXIT",
9194
],
9295
);
9396
}

validator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fs-err = { workspace = true }
2929
futures = { workspace = true }
3030
helper_functions = { workspace = true }
3131
hex = { workspace = true }
32+
http_api_utils = { workspace = true }
3233
itertools = { workspace = true }
3334
jwt-simple = { workspace = true }
3435
keymanager = { workspace = true }

validator/src/api.rs

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use anyhow::{Error as AnyhowError, Result};
99
use axum::{
1010
async_trait,
1111
body::Body,
12-
extract::{FromRef, FromRequest, FromRequestParts, Path as RequestPath, State},
12+
extract::{FromRef, FromRequest, FromRequestParts, Path as RequestPath, Query, State},
1313
headers::{authorization::Bearer, Authorization},
1414
http::{request::Parts, Request, StatusCode},
1515
middleware::Next,
@@ -20,18 +20,30 @@ use axum::{
2020
use bls::PublicKeyBytes;
2121
use directories::Directories;
2222
use educe::Educe;
23+
use eth1_api::ApiController;
24+
use fork_choice_control::Wait;
25+
use helper_functions::{accessors, signing::SignForSingleFork};
2326
use jwt_simple::{
2427
algorithms::{HS256Key, MACLike as _},
2528
claims::{JWTClaims, NoCustomClaims},
2629
reexports::coarsetime::Clock,
2730
};
2831
use keymanager::{KeyManager, KeymanagerOperationStatus, RemoteKey, ValidatingPubkey};
2932
use log::{debug, info};
30-
use serde::{Deserialize, Serialize};
33+
use serde::{de::DeserializeOwned, Deserialize, Serialize};
34+
use signer::{Signer, SigningMessage};
3135
use std_ext::ArcExt as _;
3236
use thiserror::Error;
37+
use tokio::sync::RwLock;
3338
use tower_http::cors::AllowOrigin;
34-
use types::{bellatrix::primitives::Gas, phase0::primitives::ExecutionAddress};
39+
use types::{
40+
bellatrix::primitives::Gas,
41+
phase0::{
42+
containers::{SignedVoluntaryExit, VoluntaryExit},
43+
primitives::{Epoch, ExecutionAddress},
44+
},
45+
preset::Preset,
46+
};
3547
use zeroize::Zeroizing;
3648

3749
const VALIDATOR_API_TOKEN_PATH: &str = "api-token.txt";
@@ -71,14 +83,25 @@ enum Error {
7183
InvalidJsonBody(#[source] AnyhowError),
7284
#[error("invalid public key")]
7385
InvalidPublicKey(#[source] AnyhowError),
86+
#[error("invalid query string")]
87+
InvalidQuery(#[source] AnyhowError),
7488
#[error("authentication error")]
7589
Unauthorized(#[source] AnyhowError),
90+
#[error("validator {pubkey} not found")]
91+
ValidatorNotFound { pubkey: PublicKeyBytes },
92+
#[error("validator {pubkey} is not managed by validator client")]
93+
ValidatorNotOwned { pubkey: PublicKeyBytes },
7694
}
7795

7896
impl IntoResponse for Error {
7997
fn into_response(self) -> Response {
8098
match self {
81-
Self::InvalidJsonBody(_) | Self::InvalidPublicKey(_) => StatusCode::BAD_REQUEST,
99+
Self::InvalidJsonBody(_) | Self::InvalidPublicKey(_) | Self::InvalidQuery(_) => {
100+
StatusCode::BAD_REQUEST
101+
}
102+
Self::ValidatorNotFound { pubkey: _ } | Self::ValidatorNotOwned { pubkey: _ } => {
103+
StatusCode::NOT_FOUND
104+
}
82105
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
83106
Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
84107
}
@@ -252,24 +275,54 @@ impl<S> FromRequestParts<S> for EthPath<PublicKeyBytes> {
252275
}
253276
}
254277

278+
struct EthQuery<T>(pub T);
279+
280+
#[async_trait]
281+
impl<S, T: DeserializeOwned + 'static> FromRequestParts<S> for EthQuery<T> {
282+
type Rejection = Error;
283+
284+
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
285+
parts
286+
.extract()
287+
.await
288+
.map(|Query(query)| Self(query))
289+
.map_err(AnyhowError::msg)
290+
.map_err(Error::InvalidQuery)
291+
}
292+
}
293+
255294
#[derive(Clone)]
256-
struct ValidatorApiState {
295+
struct ValidatorApiState<P: Preset, W: Wait> {
296+
controller: ApiController<P, W>,
257297
keymanager: Arc<KeyManager>,
258298
secret: Arc<Secret>,
299+
signer: Arc<RwLock<Signer>>,
300+
}
301+
302+
impl<P: Preset, W: Wait> FromRef<ValidatorApiState<P, W>> for ApiController<P, W> {
303+
fn from_ref(state: &ValidatorApiState<P, W>) -> Self {
304+
state.controller.clone_arc()
305+
}
259306
}
260307

261-
impl FromRef<ValidatorApiState> for Arc<KeyManager> {
262-
fn from_ref(state: &ValidatorApiState) -> Self {
308+
impl<P: Preset, W: Wait> FromRef<ValidatorApiState<P, W>> for Arc<KeyManager> {
309+
fn from_ref(state: &ValidatorApiState<P, W>) -> Self {
263310
state.keymanager.clone_arc()
264311
}
265312
}
266313

267-
impl FromRef<ValidatorApiState> for Arc<Secret> {
268-
fn from_ref(state: &ValidatorApiState) -> Self {
314+
impl<P: Preset, W: Wait> FromRef<ValidatorApiState<P, W>> for Arc<Secret> {
315+
fn from_ref(state: &ValidatorApiState<P, W>) -> Self {
269316
state.secret.clone_arc()
270317
}
271318
}
272319

320+
impl<P: Preset, W: Wait> FromRef<ValidatorApiState<P, W>> for Arc<RwLock<Signer>> {
321+
fn from_ref(state: &ValidatorApiState<P, W>) -> Self {
322+
state.signer.clone_arc()
323+
}
324+
}
325+
273326
#[derive(Deserialize)]
274327
struct SetFeeRecipientQuery {
275328
ethaddress: ExecutionAddress,
@@ -319,6 +372,11 @@ struct ProposerConfigResponse {
319372
graffiti: Option<String>,
320373
}
321374

375+
#[derive(Deserialize)]
376+
struct CreateVoluntaryExitQuery {
377+
epoch: Option<Epoch>,
378+
}
379+
322380
/// `GET /eth/v1/validator/{pubkey}/feerecipient`
323381
async fn keymanager_list_fee_recipient(
324382
State(keymanager): State<Arc<KeyManager>>,
@@ -518,6 +576,48 @@ async fn keymanager_delete_remote_keys(
518576
Ok(EthResponse::json(delete_statuses))
519577
}
520578

579+
/// `POST /eth/v1/validator/{pubkey}/voluntary_exit`
580+
async fn keymanager_create_voluntary_exit<P: Preset, W: Wait>(
581+
State(controller): State<ApiController<P, W>>,
582+
State(signer): State<Arc<RwLock<Signer>>>,
583+
EthPath(pubkey): EthPath<PublicKeyBytes>,
584+
EthQuery(query): EthQuery<CreateVoluntaryExitQuery>,
585+
) -> Result<EthResponse<SignedVoluntaryExit>, Error> {
586+
let state = controller.preprocessed_state_at_current_slot()?;
587+
588+
let epoch = query
589+
.epoch
590+
.unwrap_or_else(|| accessors::get_current_epoch(&state));
591+
592+
if !signer.read().await.has_key(pubkey) {
593+
return Err(Error::ValidatorNotOwned { pubkey });
594+
}
595+
596+
let validator_index = accessors::index_of_public_key(&state, pubkey)
597+
.ok_or(Error::ValidatorNotFound { pubkey })?;
598+
599+
let voluntary_exit = VoluntaryExit {
600+
epoch,
601+
validator_index,
602+
};
603+
604+
let signature = signer
605+
.read()
606+
.await
607+
.sign(
608+
SigningMessage::VoluntaryExit(voluntary_exit),
609+
voluntary_exit.signing_root(controller.chain_config(), &state),
610+
Some(state.as_ref().into()),
611+
pubkey,
612+
)
613+
.await?;
614+
615+
Ok(EthResponse::json(SignedVoluntaryExit {
616+
message: voluntary_exit,
617+
signature: signature.into(),
618+
}))
619+
}
620+
521621
async fn authorize_token(
522622
State(secret): State<Arc<Secret>>,
523623
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
@@ -534,21 +634,28 @@ async fn authorize_token(
534634
}
535635

536636
#[allow(clippy::module_name_repetitions)]
537-
pub async fn run_validator_api(
637+
pub async fn run_validator_api<P: Preset, W: Wait>(
538638
validator_api_config: ValidatorApiConfig,
539-
keymanager: Arc<KeyManager>,
639+
controller: ApiController<P, W>,
540640
directories: Arc<Directories>,
641+
keymanager: Arc<KeyManager>,
642+
signer: Arc<RwLock<Signer>>,
541643
) -> Result<()> {
542644
let Auth { secret, token } = load_or_build_auth_token(&directories)?;
543645

544-
info!(
545-
"Validator API is listening on {}, authorization token: {token}",
546-
validator_api_config.address
547-
);
646+
let ValidatorApiConfig {
647+
address,
648+
allow_origin,
649+
timeout,
650+
} = validator_api_config;
651+
652+
info!("Validator API is listening on {address}, authorization token: {token}");
548653

549654
let state = ValidatorApiState {
655+
controller,
550656
keymanager,
551657
secret: Arc::new(secret),
658+
signer,
552659
};
553660

554661
let router = eth_v1_keymanager_routes()
@@ -558,13 +665,16 @@ pub async fn run_validator_api(
558665
))
559666
.with_state(state);
560667

561-
Server::bind(&validator_api_config.address)
668+
let router =
669+
http_api_utils::extend_router_with_middleware(router, Some(timeout), allow_origin, None);
670+
671+
Server::bind(&address)
562672
.serve(router.into_make_service_with_connect_info::<SocketAddr>())
563673
.await
564674
.map_err(AnyhowError::new)
565675
}
566676

567-
fn eth_v1_keymanager_routes() -> Router<ValidatorApiState> {
677+
fn eth_v1_keymanager_routes<P: Preset, W: Wait>() -> Router<ValidatorApiState<P, W>> {
568678
Router::new()
569679
.route(
570680
"/eth/v1/validator/:pubkey/feerecipient",
@@ -602,6 +712,10 @@ fn eth_v1_keymanager_routes() -> Router<ValidatorApiState> {
602712
"/eth/v1/validator/:pubkey/graffiti",
603713
delete(keymanager_delete_graffiti),
604714
)
715+
.route(
716+
"/eth/v1/validator/:pubkey/voluntary_exit",
717+
post(keymanager_create_voluntary_exit),
718+
)
605719
.route("/eth/v1/keystores", get(keymanager_list_validating_pubkeys))
606720
.route("/eth/v1/keystores", post(keymanager_import_keystores))
607721
.route("/eth/v1/keystores", delete(keymanager_delete_keystores))

0 commit comments

Comments
 (0)