@@ -9,7 +9,7 @@ use anyhow::{Error as AnyhowError, Result};
9
9
use axum:: {
10
10
async_trait,
11
11
body:: Body ,
12
- extract:: { FromRef , FromRequest , FromRequestParts , Path as RequestPath , State } ,
12
+ extract:: { FromRef , FromRequest , FromRequestParts , Path as RequestPath , Query , State } ,
13
13
headers:: { authorization:: Bearer , Authorization } ,
14
14
http:: { request:: Parts , Request , StatusCode } ,
15
15
middleware:: Next ,
@@ -20,18 +20,30 @@ use axum::{
20
20
use bls:: PublicKeyBytes ;
21
21
use directories:: Directories ;
22
22
use educe:: Educe ;
23
+ use eth1_api:: ApiController ;
24
+ use fork_choice_control:: Wait ;
25
+ use helper_functions:: { accessors, signing:: SignForSingleFork } ;
23
26
use jwt_simple:: {
24
27
algorithms:: { HS256Key , MACLike as _} ,
25
28
claims:: { JWTClaims , NoCustomClaims } ,
26
29
reexports:: coarsetime:: Clock ,
27
30
} ;
28
31
use keymanager:: { KeyManager , KeymanagerOperationStatus , RemoteKey , ValidatingPubkey } ;
29
32
use log:: { debug, info} ;
30
- use serde:: { Deserialize , Serialize } ;
33
+ use serde:: { de:: DeserializeOwned , Deserialize , Serialize } ;
34
+ use signer:: { Signer , SigningMessage } ;
31
35
use std_ext:: ArcExt as _;
32
36
use thiserror:: Error ;
37
+ use tokio:: sync:: RwLock ;
33
38
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
+ } ;
35
47
use zeroize:: Zeroizing ;
36
48
37
49
const VALIDATOR_API_TOKEN_PATH : & str = "api-token.txt" ;
@@ -71,14 +83,25 @@ enum Error {
71
83
InvalidJsonBody ( #[ source] AnyhowError ) ,
72
84
#[ error( "invalid public key" ) ]
73
85
InvalidPublicKey ( #[ source] AnyhowError ) ,
86
+ #[ error( "invalid query string" ) ]
87
+ InvalidQuery ( #[ source] AnyhowError ) ,
74
88
#[ error( "authentication error" ) ]
75
89
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 } ,
76
94
}
77
95
78
96
impl IntoResponse for Error {
79
97
fn into_response ( self ) -> Response {
80
98
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
+ }
82
105
Self :: Internal ( _) => StatusCode :: INTERNAL_SERVER_ERROR ,
83
106
Self :: Unauthorized ( _) => StatusCode :: UNAUTHORIZED ,
84
107
}
@@ -252,24 +275,54 @@ impl<S> FromRequestParts<S> for EthPath<PublicKeyBytes> {
252
275
}
253
276
}
254
277
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
+
255
294
#[ derive( Clone ) ]
256
- struct ValidatorApiState {
295
+ struct ValidatorApiState < P : Preset , W : Wait > {
296
+ controller : ApiController < P , W > ,
257
297
keymanager : Arc < KeyManager > ,
258
298
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
+ }
259
306
}
260
307
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 {
263
310
state. keymanager . clone_arc ( )
264
311
}
265
312
}
266
313
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 {
269
316
state. secret . clone_arc ( )
270
317
}
271
318
}
272
319
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
+
273
326
#[ derive( Deserialize ) ]
274
327
struct SetFeeRecipientQuery {
275
328
ethaddress : ExecutionAddress ,
@@ -319,6 +372,11 @@ struct ProposerConfigResponse {
319
372
graffiti : Option < String > ,
320
373
}
321
374
375
+ #[ derive( Deserialize ) ]
376
+ struct CreateVoluntaryExitQuery {
377
+ epoch : Option < Epoch > ,
378
+ }
379
+
322
380
/// `GET /eth/v1/validator/{pubkey}/feerecipient`
323
381
async fn keymanager_list_fee_recipient (
324
382
State ( keymanager) : State < Arc < KeyManager > > ,
@@ -518,6 +576,48 @@ async fn keymanager_delete_remote_keys(
518
576
Ok ( EthResponse :: json ( delete_statuses) )
519
577
}
520
578
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
+
521
621
async fn authorize_token (
522
622
State ( secret) : State < Arc < Secret > > ,
523
623
TypedHeader ( auth) : TypedHeader < Authorization < Bearer > > ,
@@ -534,21 +634,28 @@ async fn authorize_token(
534
634
}
535
635
536
636
#[ allow( clippy:: module_name_repetitions) ]
537
- pub async fn run_validator_api (
637
+ pub async fn run_validator_api < P : Preset , W : Wait > (
538
638
validator_api_config : ValidatorApiConfig ,
539
- keymanager : Arc < KeyManager > ,
639
+ controller : ApiController < P , W > ,
540
640
directories : Arc < Directories > ,
641
+ keymanager : Arc < KeyManager > ,
642
+ signer : Arc < RwLock < Signer > > ,
541
643
) -> Result < ( ) > {
542
644
let Auth { secret, token } = load_or_build_auth_token ( & directories) ?;
543
645
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}" ) ;
548
653
549
654
let state = ValidatorApiState {
655
+ controller,
550
656
keymanager,
551
657
secret : Arc :: new ( secret) ,
658
+ signer,
552
659
} ;
553
660
554
661
let router = eth_v1_keymanager_routes ( )
@@ -558,13 +665,16 @@ pub async fn run_validator_api(
558
665
) )
559
666
. with_state ( state) ;
560
667
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)
562
672
. serve ( router. into_make_service_with_connect_info :: < SocketAddr > ( ) )
563
673
. await
564
674
. map_err ( AnyhowError :: new)
565
675
}
566
676
567
- fn eth_v1_keymanager_routes ( ) -> Router < ValidatorApiState > {
677
+ fn eth_v1_keymanager_routes < P : Preset , W : Wait > ( ) -> Router < ValidatorApiState < P , W > > {
568
678
Router :: new ( )
569
679
. route (
570
680
"/eth/v1/validator/:pubkey/feerecipient" ,
@@ -602,6 +712,10 @@ fn eth_v1_keymanager_routes() -> Router<ValidatorApiState> {
602
712
"/eth/v1/validator/:pubkey/graffiti" ,
603
713
delete ( keymanager_delete_graffiti) ,
604
714
)
715
+ . route (
716
+ "/eth/v1/validator/:pubkey/voluntary_exit" ,
717
+ post ( keymanager_create_voluntary_exit) ,
718
+ )
605
719
. route ( "/eth/v1/keystores" , get ( keymanager_list_validating_pubkeys) )
606
720
. route ( "/eth/v1/keystores" , post ( keymanager_import_keystores) )
607
721
. route ( "/eth/v1/keystores" , delete ( keymanager_delete_keystores) )
0 commit comments