mod api_secret; mod create_signed_voluntary_exit; mod create_validator; mod graffiti; mod keystores; mod remotekeys; mod tests; pub mod test_utils; pub use api_secret::PK_FILENAME; use graffiti::{delete_graffiti, get_graffiti, set_graffiti}; use create_signed_voluntary_exit::create_signed_voluntary_exit; use graffiti_file::{determine_graffiti, GraffitiFile}; use validator_store::ValidatorStore; use account_utils::{ mnemonic_from_phrase, validator_definitions::{SigningDefinition, ValidatorDefinition, Web3SignerDefinition}, }; pub use api_secret::ApiSecret; use beacon_node_fallback::CandidateInfo; use create_validator::{ create_validators_mnemonic, create_validators_web3signer, get_voting_password_storage, }; use directory::{DEFAULT_HARDCODED_NETWORK, DEFAULT_ROOT_DIR, DEFAULT_VALIDATOR_DIR}; use eth2::lighthouse_vc::{ std_types::{AuthResponse, GetFeeRecipientResponse, GetGasLimitResponse}, types::{ self as api_types, GenericResponse, GetGraffitiResponse, Graffiti, PublicKey, PublicKeyBytes, SetGraffitiRequest, }, }; use health_metrics::observe::Observe; use lighthouse_version::version_with_platform; use logging::crit; use logging::SSELoggingComponents; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use std::collections::HashMap; use std::future::Future; use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use sysinfo::{System, SystemExt}; use system_health::observe_system_health_vc; use task_executor::TaskExecutor; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use tracing::{info, warn}; use types::{ChainSpec, ConfigAndPreset, EthSpec}; use validator_dir::Builder as ValidatorDirBuilder; use validator_services::block_service::BlockService; use warp::{sse::Event, Filter}; use warp_utils::task::blocking_json_task; #[derive(Debug)] pub enum Error { Warp(warp::Error), Other(String), } impl From for Error { fn from(e: warp::Error) -> Self { Error::Warp(e) } } impl From for Error { fn from(e: String) -> Self { Error::Other(e) } } /// A wrapper around all the items required to spawn the HTTP server. /// /// The server will gracefully handle the case where any fields are `None`. pub struct Context { pub task_executor: TaskExecutor, pub api_secret: ApiSecret, pub block_service: Option>, pub validator_store: Option>>, pub validator_dir: Option, pub secrets_dir: Option, pub graffiti_file: Option, pub graffiti_flag: Option, pub spec: Arc, pub config: Config, pub sse_logging_components: Option, pub slot_clock: T, pub _phantom: PhantomData, } /// Configuration for the HTTP server. #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct Config { pub enabled: bool, pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option, pub allow_keystore_export: bool, pub store_passwords_in_secrets_dir: bool, pub http_token_path: PathBuf, } impl Default for Config { fn default() -> Self { // This value is always overridden when building config from CLI. let http_token_path = dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join(DEFAULT_ROOT_DIR) .join(DEFAULT_HARDCODED_NETWORK) .join(DEFAULT_VALIDATOR_DIR) .join(PK_FILENAME); Self { enabled: false, listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5062, allow_origin: None, allow_keystore_export: false, store_passwords_in_secrets_dir: false, http_token_path, } } } /// Creates a server that will serve requests using information from `ctx`. /// /// The server will shut down gracefully when the `shutdown` future resolves. /// /// ## Returns /// /// This function will bind the server to the provided address and then return a tuple of: /// /// - `SocketAddr`: the address that the HTTP server will listen on. /// - `Future`: the actual server future that will need to be awaited. /// /// ## Errors /// /// Returns an error if the server is unable to bind or there is another error during /// configuration. pub fn serve( ctx: Arc>, shutdown: impl Future + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; let allow_keystore_export = config.allow_keystore_export; let store_passwords_in_secrets_dir = config.store_passwords_in_secrets_dir; // Configure CORS. let cors_builder = { let builder = warp::cors() .allow_methods(vec!["GET", "POST", "PATCH", "DELETE"]) .allow_headers(vec!["Content-Type", "Authorization"]); warp_utils::cors::set_builder_origins( builder, config.allow_origin.as_deref(), (config.listen_addr, config.listen_port), )? }; // Sanity check. if !config.enabled { crit!("Cannot start disabled metrics HTTP server"); return Err(Error::Other( "A disabled metrics server should not be started".to_string(), )); } let authorization_header_filter = ctx.api_secret.authorization_header_filter(); let mut api_token_path = ctx.api_secret.api_token_path(); // Attempt to convert the path to an absolute path, but don't error if it fails. match api_token_path.canonicalize() { Ok(abs_path) => api_token_path = abs_path, Err(e) => { warn!( error = ?e, "Error canonicalizing token path" ); } }; let inner_block_service = ctx.block_service.clone(); let block_service_filter = warp::any() .map(move || inner_block_service.clone()) .and_then(|block_service: Option<_>| async move { block_service.ok_or_else(|| { warp_utils::reject::custom_not_found( "block service is not initialized.".to_string(), ) }) }); let inner_validator_store = ctx.validator_store.clone(); let validator_store_filter = warp::any() .map(move || inner_validator_store.clone()) .and_then(|validator_store: Option<_>| async move { validator_store.ok_or_else(|| { warp_utils::reject::custom_not_found( "validator store is not initialized.".to_string(), ) }) }); let inner_task_executor = ctx.task_executor.clone(); let task_executor_filter = warp::any().map(move || inner_task_executor.clone()); let inner_validator_dir = ctx.validator_dir.clone(); let validator_dir_filter = warp::any() .map(move || inner_validator_dir.clone()) .and_then(|validator_dir: Option<_>| async move { validator_dir.ok_or_else(|| { warp_utils::reject::custom_not_found( "validator_dir directory is not initialized.".to_string(), ) }) }); let inner_secrets_dir = ctx.secrets_dir.clone(); let secrets_dir_filter = warp::any().map(move || inner_secrets_dir.clone()).and_then( |secrets_dir: Option<_>| async move { secrets_dir.ok_or_else(|| { warp_utils::reject::custom_not_found( "secrets_dir directory is not initialized.".to_string(), ) }) }, ); let inner_graffiti_file = ctx.graffiti_file.clone(); let graffiti_file_filter = warp::any().map(move || inner_graffiti_file.clone()); let inner_graffiti_flag = ctx.graffiti_flag; let graffiti_flag_filter = warp::any().map(move || inner_graffiti_flag); let inner_slot_clock = ctx.slot_clock.clone(); let slot_clock_filter = warp::any().map(move || inner_slot_clock.clone()); let inner_spec = ctx.spec.clone(); let spec_filter = warp::any().map(move || inner_spec.clone()); let api_token_path_inner = api_token_path.clone(); let api_token_path_filter = warp::any().map(move || api_token_path_inner.clone()); // Filter for SEE Logging events let inner_components = ctx.sse_logging_components.clone(); let sse_component_filter = warp::any().map(move || inner_components.clone()); // Create a `warp` filter that provides access to local system information. let system_info = Arc::new(RwLock::new(sysinfo::System::new())); { // grab write access for initialisation let mut system_info = system_info.write(); system_info.refresh_disks_list(); system_info.refresh_networks_list(); } // end lock let system_info_filter = warp::any() .map(move || system_info.clone()) .map(|sysinfo: Arc>| { { // refresh stats let mut sysinfo_lock = sysinfo.write(); sysinfo_lock.refresh_memory(); sysinfo_lock.refresh_cpu_specifics(sysinfo::CpuRefreshKind::everything()); sysinfo_lock.refresh_cpu(); sysinfo_lock.refresh_system(); sysinfo_lock.refresh_networks(); sysinfo_lock.refresh_disks(); } // end lock sysinfo }); let app_start = std::time::Instant::now(); let app_start_filter = warp::any().map(move || app_start); // GET lighthouse/version let get_node_version = warp::path("lighthouse") .and(warp::path("version")) .and(warp::path::end()) .then(|| { blocking_json_task(move || { Ok(api_types::GenericResponse::from(api_types::VersionData { version: version_with_platform(), })) }) }); // GET lighthouse/health let get_lighthouse_health = warp::path("lighthouse") .and(warp::path("health")) .and(warp::path::end()) .then(|| { blocking_json_task(move || { eth2::lighthouse::Health::observe() .map(api_types::GenericResponse::from) .map_err(warp_utils::reject::custom_bad_request) }) }); // GET lighthouse/spec let get_lighthouse_spec = warp::path("lighthouse") .and(warp::path("spec")) .and(warp::path::end()) .and(spec_filter.clone()) .then(|spec: Arc<_>| { blocking_json_task(move || { let config = ConfigAndPreset::from_chain_spec::(&spec, None); Ok(api_types::GenericResponse::from(config)) }) }); // GET lighthouse/validators let get_lighthouse_validators = warp::path("lighthouse") .and(warp::path("validators")) .and(warp::path::end()) .and(validator_store_filter.clone()) .then(|validator_store: Arc>| { blocking_json_task(move || { let validators = validator_store .initialized_validators() .read() .validator_definitions() .iter() .map(|def| api_types::ValidatorData { enabled: def.enabled, description: def.description.clone(), voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), }) .collect::>(); Ok(api_types::GenericResponse::from(validators)) }) }); // GET lighthouse/validators/{validator_pubkey} let get_lighthouse_validators_pubkey = warp::path("lighthouse") .and(warp::path("validators")) .and(warp::path::param::()) .and(warp::path::end()) .and(validator_store_filter.clone()) .then( |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { let validator = validator_store .initialized_validators() .read() .validator_definitions() .iter() .find(|def| def.voting_public_key == validator_pubkey) .map(|def| api_types::ValidatorData { enabled: def.enabled, description: def.description.clone(), voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), }) .ok_or_else(|| { warp_utils::reject::custom_not_found(format!( "no validator for {:?}", validator_pubkey )) })?; Ok(api_types::GenericResponse::from(validator)) }) }, ); // GET lighthouse/ui/health let get_lighthouse_ui_health = warp::path("lighthouse") .and(warp::path("ui")) .and(warp::path("health")) .and(warp::path::end()) .and(system_info_filter) .and(app_start_filter) .and(validator_dir_filter.clone()) .then(|sysinfo, app_start: std::time::Instant, val_dir| { blocking_json_task(move || { let app_uptime = app_start.elapsed().as_secs(); Ok(api_types::GenericResponse::from(observe_system_health_vc( sysinfo, val_dir, app_uptime, ))) }) }); let get_lighthouse_ui_graffiti = warp::path("lighthouse") .and(warp::path("ui")) .and(warp::path("graffiti")) .and(warp::path::end()) .and(validator_store_filter.clone()) .and(graffiti_file_filter.clone()) .and(graffiti_flag_filter) .then( |validator_store: Arc>, graffiti_file: Option, graffiti_flag: Option| { blocking_json_task(move || { let mut result = HashMap::new(); for (key, graffiti_definition) in validator_store .initialized_validators() .read() .get_all_validators_graffiti() { let graffiti = determine_graffiti( key, graffiti_file.clone(), graffiti_definition, graffiti_flag, ); result.insert(key.to_string(), graffiti.map(|g| g.as_utf8_lossy())); } Ok(api_types::GenericResponse::from(result)) }) }, ); // GET lighthouse/ui/fallback_health let get_lighthouse_ui_fallback_health = warp::path("lighthouse") .and(warp::path("ui")) .and(warp::path("fallback_health")) .and(warp::path::end()) .and(block_service_filter.clone()) .then(|block_filter: BlockService| async move { let mut result: HashMap> = HashMap::new(); let mut beacon_nodes = Vec::new(); for node in &*block_filter.beacon_nodes.candidates.read().await { beacon_nodes.push(CandidateInfo { index: node.index, endpoint: node.beacon_node.to_string(), health: *node.health.read().await, }); } result.insert("beacon_nodes".to_string(), beacon_nodes); if let Some(proposer_nodes_list) = &block_filter.proposer_nodes { let mut proposer_nodes = Vec::new(); for node in &*proposer_nodes_list.candidates.read().await { proposer_nodes.push(CandidateInfo { index: node.index, endpoint: node.beacon_node.to_string(), health: *node.health.read().await, }); } result.insert("proposer_nodes".to_string(), proposer_nodes); } blocking_json_task(move || Ok(api_types::GenericResponse::from(result))).await }); // POST lighthouse/validators/ let post_validators = warp::path("lighthouse") .and(warp::path("validators")) .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(spec_filter.clone()) .and(task_executor_filter.clone()) .then( move |body: Vec, validator_dir: PathBuf, secrets_dir: PathBuf, validator_store: Arc>, spec: Arc, task_executor: TaskExecutor| { blocking_json_task(move || { let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); if let Some(handle) = task_executor.handle() { let (validators, mnemonic) = handle.block_on(create_validators_mnemonic( None, None, &body, &validator_dir, secrets_dir, &validator_store, &spec, ))?; let response = api_types::PostValidatorsResponseData { mnemonic: mnemonic.into_phrase().into(), validators, }; Ok(api_types::GenericResponse::from(response)) } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) } }) }, ); // POST lighthouse/validators/mnemonic let post_validators_mnemonic = warp::path("lighthouse") .and(warp::path("validators")) .and(warp::path("mnemonic")) .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(spec_filter) .and(task_executor_filter.clone()) .then( move |body: api_types::CreateValidatorsMnemonicRequest, validator_dir: PathBuf, secrets_dir: PathBuf, validator_store: Arc>, spec: Arc, task_executor: TaskExecutor| { blocking_json_task(move || { let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); if let Some(handle) = task_executor.handle() { let mnemonic = mnemonic_from_phrase(body.mnemonic.as_str()).map_err(|e| { warp_utils::reject::custom_bad_request(format!( "invalid mnemonic: {:?}", e )) })?; let (validators, _mnemonic) = handle.block_on(create_validators_mnemonic( Some(mnemonic), Some(body.key_derivation_path_offset), &body.validators, &validator_dir, secrets_dir, &validator_store, &spec, ))?; Ok(api_types::GenericResponse::from(validators)) } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) } }) }, ); // POST lighthouse/validators/keystore let post_validators_keystore = warp::path("lighthouse") .and(warp::path("validators")) .and(warp::path("keystore")) .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then( move |body: api_types::KeystoreValidatorsPostRequest, validator_dir: PathBuf, secrets_dir: PathBuf, validator_store: Arc>, task_executor: TaskExecutor| { blocking_json_task(move || { // Check to ensure the password is correct. let keypair = body .keystore .decrypt_keypair(body.password.as_ref()) .map_err(|e| { warp_utils::reject::custom_bad_request(format!( "invalid keystore: {:?}", e )) })?; let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); let password_storage = get_voting_password_storage(&secrets_dir, &body.keystore, &body.password)?; let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) .password_dir_opt(secrets_dir) .voting_keystore(body.keystore.clone(), body.password.as_ref()) .store_withdrawal_keystore(false) .build() .map_err(|e| { warp_utils::reject::custom_server_error(format!( "failed to build validator directory: {:?}", e )) })?; // Drop validator dir so that `add_validator_keystore` can re-lock the keystore. let voting_keystore_path = validator_dir.voting_keystore_path(); drop(validator_dir); let graffiti = body.graffiti.clone(); let suggested_fee_recipient = body.suggested_fee_recipient; let gas_limit = body.gas_limit; let builder_proposals = body.builder_proposals; let builder_boost_factor = body.builder_boost_factor; let prefer_builder_proposals = body.prefer_builder_proposals; let validator_def = { if let Some(handle) = task_executor.handle() { handle .block_on(validator_store.add_validator_keystore( voting_keystore_path, password_storage, body.enable, graffiti, suggested_fee_recipient, gas_limit, builder_proposals, builder_boost_factor, prefer_builder_proposals, )) .map_err(|e| { warp_utils::reject::custom_server_error(format!( "failed to initialize validator: {:?}", e )) })? } else { return Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )); } }; Ok(api_types::GenericResponse::from(api_types::ValidatorData { enabled: body.enable, description: validator_def.description, voting_pubkey: keypair.pk.into(), })) }) }, ); // POST lighthouse/validators/web3signer let post_validators_web3signer = warp::path("lighthouse") .and(warp::path("validators")) .and(warp::path("web3signer")) .and(warp::path::end()) .and(warp::body::json()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then( |body: Vec, validator_store: Arc>, task_executor: TaskExecutor| { blocking_json_task(move || { if let Some(handle) = task_executor.handle() { let web3signers: Vec = body .into_iter() .map(|web3signer| ValidatorDefinition { enabled: web3signer.enable, voting_public_key: web3signer.voting_public_key, graffiti: web3signer.graffiti, suggested_fee_recipient: web3signer.suggested_fee_recipient, gas_limit: web3signer.gas_limit, builder_proposals: web3signer.builder_proposals, builder_boost_factor: web3signer.builder_boost_factor, prefer_builder_proposals: web3signer.prefer_builder_proposals, description: web3signer.description, signing_definition: SigningDefinition::Web3Signer( Web3SignerDefinition { url: web3signer.url, root_certificate_path: web3signer.root_certificate_path, request_timeout_ms: web3signer.request_timeout_ms, client_identity_path: web3signer.client_identity_path, client_identity_password: web3signer .client_identity_password, }, ), }) .collect(); handle.block_on(create_validators_web3signer( web3signers, &validator_store, ))?; Ok(()) } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) } }) }, ); // PATCH lighthouse/validators/{validator_pubkey} let patch_validators = warp::path("lighthouse") .and(warp::path("validators")) .and(warp::path::param::()) .and(warp::path::end()) .and(warp::body::json()) .and(validator_store_filter.clone()) .and(graffiti_file_filter.clone()) .and(task_executor_filter.clone()) .then( |validator_pubkey: PublicKey, body: api_types::ValidatorPatchRequest, validator_store: Arc>, graffiti_file: Option, task_executor: TaskExecutor| { blocking_json_task(move || { if body.graffiti.is_some() && graffiti_file.is_some() { return Err(warp_utils::reject::custom_bad_request( "Unable to update graffiti as the \"--graffiti-file\" flag is set" .to_string(), )); } let maybe_graffiti = body.graffiti.clone().map(Into::into); let initialized_validators_rw_lock = validator_store.initialized_validators(); let initialized_validators = initialized_validators_rw_lock.upgradable_read(); // Do not make any changes if all fields are identical or unchanged. fn equal_or_none( current_value: Option, new_value: Option, ) -> bool { new_value.is_none() || current_value == new_value } match ( initialized_validators.is_enabled(&validator_pubkey), initialized_validators.validator(&validator_pubkey.compress()), ) { (None, _) => Err(warp_utils::reject::custom_not_found(format!( "no validator for {:?}", validator_pubkey ))), // If all specified parameters match their existing settings, then this // change is a no-op. (Some(is_enabled), Some(initialized_validator)) if equal_or_none(Some(is_enabled), body.enabled) && equal_or_none( initialized_validator.get_gas_limit(), body.gas_limit, ) && equal_or_none( initialized_validator.get_builder_boost_factor(), body.builder_boost_factor, ) && equal_or_none( initialized_validator.get_builder_proposals(), body.builder_proposals, ) && equal_or_none( initialized_validator.get_prefer_builder_proposals(), body.prefer_builder_proposals, ) && equal_or_none( initialized_validator.get_graffiti(), maybe_graffiti, ) => { Ok(()) } // Disabling an already disabled validator *with no other changes* is a // no-op. (Some(false), None) if body.enabled.is_none_or(|enabled| !enabled) && body.gas_limit.is_none() && body.builder_boost_factor.is_none() && body.builder_proposals.is_none() && body.prefer_builder_proposals.is_none() && maybe_graffiti.is_none() => { Ok(()) } (Some(_), _) => { // Upgrade read lock only in the case where a write is actually // required. let mut initialized_validators_write = parking_lot::RwLockUpgradableReadGuard::upgrade( initialized_validators, ); if let Some(handle) = task_executor.handle() { handle .block_on( initialized_validators_write .set_validator_definition_fields( &validator_pubkey, body.enabled, body.gas_limit, body.builder_proposals, body.builder_boost_factor, body.prefer_builder_proposals, body.graffiti, ), ) .map_err(|e| { warp_utils::reject::custom_server_error(format!( "unable to set validator status: {:?}", e )) })?; Ok(()) } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) } } } }) }, ); // GET /lighthouse/auth let get_auth = warp::path("lighthouse").and(warp::path("auth").and(warp::path::end())); let get_auth = get_auth .and(api_token_path_filter) .then(move |token_path: PathBuf| { blocking_json_task(move || { Ok(AuthResponse { token_path: token_path.display().to_string(), }) }) }); // DELETE /lighthouse/keystores let delete_lighthouse_keystores = warp::path("lighthouse") .and(warp::path("keystores")) .and(warp::path::end()) .and(warp::body::json()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then(move |request, validator_store, task_executor| { blocking_json_task(move || { if allow_keystore_export { keystores::export(request, validator_store, task_executor) } else { Err(warp_utils::reject::custom_bad_request( "keystore export is disabled".to_string(), )) } }) }); // Standard key-manager endpoints. let eth_v1 = warp::path("eth").and(warp::path("v1")); let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end()); let std_remotekeys = eth_v1.and(warp::path("remotekeys")).and(warp::path::end()); // GET /eth/v1/validator/{pubkey}/feerecipient let get_fee_recipient = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) .and(warp::path("feerecipient")) .and(warp::path::end()) .and(validator_store_filter.clone()) .then( |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() .read() .is_enabled(&validator_pubkey) .is_none() { return Err(warp_utils::reject::custom_not_found(format!( "no validator found with pubkey {:?}", validator_pubkey ))); } validator_store .get_fee_recipient(&PublicKeyBytes::from(&validator_pubkey)) .map(|fee_recipient| { GenericResponse::from(GetFeeRecipientResponse { pubkey: PublicKeyBytes::from(validator_pubkey.clone()), ethaddress: fee_recipient, }) }) .ok_or_else(|| { warp_utils::reject::custom_server_error( "no fee recipient set".to_string(), ) }) }) }, ); // POST /eth/v1/validator/{pubkey}/feerecipient let post_fee_recipient = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) .and(warp::path("feerecipient")) .and(warp::body::json()) .and(warp::path::end()) .and(validator_store_filter.clone()) .then( |validator_pubkey: PublicKey, request: api_types::UpdateFeeRecipientRequest, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() .read() .is_enabled(&validator_pubkey) .is_none() { return Err(warp_utils::reject::custom_not_found(format!( "no validator found with pubkey {:?}", validator_pubkey ))); } validator_store .initialized_validators() .write() .set_validator_fee_recipient(&validator_pubkey, request.ethaddress) .map_err(|e| { warp_utils::reject::custom_server_error(format!( "Error persisting fee recipient: {:?}", e )) }) }) }, ) .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::ACCEPTED)); // DELETE /eth/v1/validator/{pubkey}/feerecipient let delete_fee_recipient = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) .and(warp::path("feerecipient")) .and(warp::path::end()) .and(validator_store_filter.clone()) .then( |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() .read() .is_enabled(&validator_pubkey) .is_none() { return Err(warp_utils::reject::custom_not_found(format!( "no validator found with pubkey {:?}", validator_pubkey ))); } validator_store .initialized_validators() .write() .delete_validator_fee_recipient(&validator_pubkey) .map_err(|e| { warp_utils::reject::custom_server_error(format!( "Error persisting fee recipient removal: {:?}", e )) }) }) }, ) .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NO_CONTENT)); // GET /eth/v1/validator/{pubkey}/gas_limit let get_gas_limit = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) .and(warp::path("gas_limit")) .and(warp::path::end()) .and(validator_store_filter.clone()) .then( |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() .read() .is_enabled(&validator_pubkey) .is_none() { return Err(warp_utils::reject::custom_not_found(format!( "no validator found with pubkey {:?}", validator_pubkey ))); } Ok(GenericResponse::from(GetGasLimitResponse { pubkey: PublicKeyBytes::from(validator_pubkey.clone()), gas_limit: validator_store .get_gas_limit(&PublicKeyBytes::from(&validator_pubkey)), })) }) }, ); // POST /eth/v1/validator/{pubkey}/gas_limit let post_gas_limit = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) .and(warp::path("gas_limit")) .and(warp::body::json()) .and(warp::path::end()) .and(validator_store_filter.clone()) .then( |validator_pubkey: PublicKey, request: api_types::UpdateGasLimitRequest, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() .read() .is_enabled(&validator_pubkey) .is_none() { return Err(warp_utils::reject::custom_not_found(format!( "no validator found with pubkey {:?}", validator_pubkey ))); } validator_store .initialized_validators() .write() .set_validator_gas_limit(&validator_pubkey, request.gas_limit) .map_err(|e| { warp_utils::reject::custom_server_error(format!( "Error persisting gas limit: {:?}", e )) }) }) }, ) .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::ACCEPTED)); // DELETE /eth/v1/validator/{pubkey}/gas_limit let delete_gas_limit = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) .and(warp::path("gas_limit")) .and(warp::path::end()) .and(validator_store_filter.clone()) .then( |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() .read() .is_enabled(&validator_pubkey) .is_none() { return Err(warp_utils::reject::custom_not_found(format!( "no validator found with pubkey {:?}", validator_pubkey ))); } validator_store .initialized_validators() .write() .delete_validator_gas_limit(&validator_pubkey) .map_err(|e| { warp_utils::reject::custom_server_error(format!( "Error persisting gas limit removal: {:?}", e )) }) }) }, ) .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NO_CONTENT)); // POST /eth/v1/validator/{pubkey}/voluntary_exit let post_validators_voluntary_exits = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) .and(warp::path("voluntary_exit")) .and(warp::query::()) .and(warp::path::end()) .and(validator_store_filter.clone()) .and(slot_clock_filter) .and(task_executor_filter.clone()) .then( |pubkey: PublicKey, query: api_types::VoluntaryExitQuery, validator_store: Arc>, slot_clock: T, task_executor: TaskExecutor| { blocking_json_task(move || { if let Some(handle) = task_executor.handle() { let signed_voluntary_exit = handle.block_on(create_signed_voluntary_exit( pubkey, query.epoch, validator_store, slot_clock, ))?; Ok(signed_voluntary_exit) } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) } }) }, ); // GET /eth/v1/validator/{pubkey}/graffiti let get_graffiti = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) .and(warp::path("graffiti")) .and(warp::path::end()) .and(validator_store_filter.clone()) .and(graffiti_flag_filter) .then( |pubkey: PublicKey, validator_store: Arc>, graffiti_flag: Option| { blocking_json_task(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::()) .and(warp::path("graffiti")) .and(warp::body::json()) .and(warp::path::end()) .and(validator_store_filter.clone()) .and(graffiti_file_filter.clone()) .then( |pubkey: PublicKey, query: SetGraffitiRequest, validator_store: Arc>, graffiti_file: Option| { blocking_json_task(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::()) .and(warp::path("graffiti")) .and(warp::path::end()) .and(validator_store_filter.clone()) .and(graffiti_file_filter.clone()) .then( |pubkey: PublicKey, validator_store: Arc>, graffiti_file: Option| { blocking_json_task(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(validator_store_filter.clone()).then( |validator_store: Arc>| { blocking_json_task(move || Ok(keystores::list(validator_store))) }, ); // POST /eth/v1/keystores let post_std_keystores = std_keystores .and(warp::body::json()) .and(validator_dir_filter) .and(secrets_dir_filter) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then( move |request, validator_dir, secrets_dir, validator_store, task_executor| { let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); blocking_json_task(move || { keystores::import( request, validator_dir, secrets_dir, validator_store, task_executor, ) }) }, ); // DELETE /eth/v1/keystores let delete_std_keystores = std_keystores .and(warp::body::json()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then(|request, validator_store, task_executor| { blocking_json_task(move || keystores::delete(request, validator_store, task_executor)) }); // GET /eth/v1/remotekeys let get_std_remotekeys = std_remotekeys.and(validator_store_filter.clone()).then( |validator_store: Arc>| { blocking_json_task(move || Ok(remotekeys::list(validator_store))) }, ); // POST /eth/v1/remotekeys let post_std_remotekeys = std_remotekeys .and(warp::body::json()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then(|request, validator_store, task_executor| { blocking_json_task(move || remotekeys::import(request, validator_store, task_executor)) }); // DELETE /eth/v1/remotekeys let delete_std_remotekeys = std_remotekeys .and(warp::body::json()) .and(validator_store_filter) .and(task_executor_filter) .then(|request, validator_store, task_executor| { blocking_json_task(move || remotekeys::delete(request, validator_store, task_executor)) }); // Subscribe to get VC logs via Server side events // /lighthouse/logs let get_log_events = warp::path("lighthouse") .and(warp::path("logs")) .and(warp::path::end()) .and(sse_component_filter) .and_then(|sse_component: Option| { warp_utils::task::blocking_task(move || { if let Some(logging_components) = sse_component { // Build a JSON stream let s = BroadcastStream::new(logging_components.sender.subscribe()).map(|msg| { match msg { Ok(data) => { // Serialize to json match serde_json::to_string(&data) .map_err(|e| format!("{:?}", e)) { // Send the json as a Server Sent Event Ok(json) => Event::default().json_data(json).map_err(|e| { warp_utils::reject::server_sent_event_error(format!( "{:?}", e )) }), Err(e) => Err(warp_utils::reject::server_sent_event_error( format!("Unable to serialize to JSON {}", e), )), } } Err(e) => Err(warp_utils::reject::server_sent_event_error( format!("Unable to receive event {}", e), )), } }); Ok::<_, warp::Rejection>(warp::sse::reply(warp::sse::keep_alive().stream(s))) } else { Err(warp_utils::reject::custom_server_error( "SSE Logging is not enabled".to_string(), )) } }) }); let routes = warp::any() .and(authorization_header_filter) // Note: it is critical that the `authorization_header_filter` is applied to all routes. // Keeping all the routes inside the following `and` is a reliable way to achieve this. // // When adding a route, don't forget to add it to the `routes_with_invalid_auth` tests! .and( warp::get() .and( get_node_version .or(get_lighthouse_health) .or(get_lighthouse_spec) .or(get_lighthouse_validators) .or(get_lighthouse_validators_pubkey) .or(get_lighthouse_ui_health) .or(get_lighthouse_ui_graffiti) .or(get_lighthouse_ui_fallback_health) .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), ) .or(warp::post().and( post_validators .or(post_validators_keystore) .or(post_validators_mnemonic) .or(post_validators_web3signer) .or(post_validators_voluntary_exits) .or(post_fee_recipient) .or(post_gas_limit) .or(post_std_keystores) .or(post_std_remotekeys) .or(post_graffiti) .recover(warp_utils::reject::handle_rejection), )) .or(warp::patch() .and(patch_validators.recover(warp_utils::reject::handle_rejection))) .or(warp::delete().and( delete_lighthouse_keystores .or(delete_fee_recipient) .or(delete_gas_limit) .or(delete_std_keystores) .or(delete_std_remotekeys) .or(delete_graffiti) .recover(warp_utils::reject::handle_rejection), )), ) // The auth route and logs are the only routes that are allowed to be accessed without the API token. .or(warp::get().and(get_auth.or(get_log_events.boxed()))) // Maps errors into HTTP responses. .recover(warp_utils::reject::handle_rejection) // Add a `Server` header. .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) .with(cors_builder.build()); let (listening_socket, server) = warp::serve(routes).try_bind_with_graceful_shutdown( SocketAddr::new(config.listen_addr, config.listen_port), async { shutdown.await; }, )?; info!( listen_address = listening_socket.to_string(), ?api_token_path, "HTTP API started" ); Ok((listening_socket, server)) }