diff --git a/book/src/builders.md b/book/src/builders.md index 1a034e082..110f2450b 100644 --- a/book/src/builders.md +++ b/book/src/builders.md @@ -59,8 +59,16 @@ so a discrepancy in fee recipient might not indicate that there is something afo only create blocks with a `fee_recipient` field matching the one suggested, you can use the [strict fee recipient](suggested-fee-recipient.md#strict-fee-recipient) flag. -### Enable/Disable builder proposals and set Gas Limit -Use the [lighthouse API](api-vc-endpoints.md) to configure these fields per-validator. +### Set Gas Limit via HTTP + +To update gas limit per-validator you can use the [standard key manager API][gas-limit-api]. + +Alternatively, you can use the [lighthouse API](api-vc-endpoints.md). See below for an example. + +### Enable/Disable builder proposals via HTTP + +Use the [lighthouse API](api-vc-endpoints.md) to enable/disable use of the builder API on a per-validator basis. +You can also update the configured gas limit with these requests. #### `PATCH /lighthouse/validators/:voting_pubkey` @@ -99,6 +107,9 @@ null Refer to [suggested fee recipient](suggested-fee-recipient.md) documentation. ### Validator definitions example + +You can also directly configure these fields in the `validator_definitions.yml` file. + ``` --- - enabled: true @@ -142,3 +153,4 @@ By default, Lighthouse is strict with these conditions, but we encourage users t [mev-rs]: https://github.com/ralexstokes/mev-rs [mev-boost]: https://github.com/flashbots/mev-boost +[gas-limit-api]: https://ethereum.github.io/keymanager-APIs/#/Gas%20Limit diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index 5f83e81aa..88b5b6840 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -519,6 +519,18 @@ impl ValidatorClientHttpClient { Ok(url) } + fn make_gas_limit_url(&self, pubkey: &PublicKeyBytes) -> Result { + 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("gas_limit"); + Ok(url) + } + /// `GET lighthouse/auth` pub async fn get_auth(&self) -> Result { let mut url = self.server.full.clone(); @@ -598,11 +610,38 @@ impl ValidatorClientHttpClient { self.post_with_raw_response(url, req).await } - /// `POST /eth/v1/validator/{pubkey}/feerecipient` + /// `DELETE /eth/v1/validator/{pubkey}/feerecipient` pub async fn delete_fee_recipient(&self, pubkey: &PublicKeyBytes) -> Result { let url = self.make_fee_recipient_url(pubkey)?; self.delete_with_raw_response(url, &()).await } + + /// `GET /eth/v1/validator/{pubkey}/gas_limit` + pub async fn get_gas_limit( + &self, + pubkey: &PublicKeyBytes, + ) -> Result { + let url = self.make_gas_limit_url(pubkey)?; + self.get(url) + .await + .map(|generic: GenericResponse| generic.data) + } + + /// `POST /eth/v1/validator/{pubkey}/gas_limit` + pub async fn post_gas_limit( + &self, + pubkey: &PublicKeyBytes, + req: &UpdateGasLimitRequest, + ) -> Result { + let url = self.make_gas_limit_url(pubkey)?; + self.post_with_raw_response(url, req).await + } + + /// `DELETE /eth/v1/validator/{pubkey}/gas_limit` + pub async fn delete_gas_limit(&self, pubkey: &PublicKeyBytes) -> Result { + let url = self.make_gas_limit_url(pubkey)?; + self.delete_with_raw_response(url, &()).await + } } /// Returns `Ok(response)` if the response is a `200 OK` response or a diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs index 62987c136..887bcb99e 100644 --- a/common/eth2/src/lighthouse_vc/std_types.rs +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -10,6 +10,13 @@ pub struct GetFeeRecipientResponse { pub ethaddress: Address, } +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub struct GetGasLimitResponse { + pub pubkey: PublicKeyBytes, + #[serde(with = "eth2_serde_utils::quoted_u64")] + pub gas_limit: u64, +} + #[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct AuthResponse { pub token_path: String, diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index d829c97cc..92439337f 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -138,3 +138,9 @@ pub struct Web3SignerValidatorRequest { pub struct UpdateFeeRecipientRequest { pub ethaddress: Address, } + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct UpdateGasLimitRequest { + #[serde(with = "eth2_serde_utils::quoted_u64")] + pub gas_limit: u64, +} diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index 1e48e86c0..e9c7bf69d 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -12,7 +12,7 @@ use account_utils::{ pub use api_secret::ApiSecret; use create_validator::{create_validators_mnemonic, create_validators_web3signer}; use eth2::lighthouse_vc::{ - std_types::{AuthResponse, GetFeeRecipientResponse}, + std_types::{AuthResponse, GetFeeRecipientResponse, GetGasLimitResponse}, types::{self as api_types, GenericResponse, PublicKey, PublicKeyBytes}, }; use lighthouse_version::version_with_platform; @@ -626,8 +626,8 @@ pub fn serve( let post_fee_recipient = eth_v1 .and(warp::path("validator")) .and(warp::path::param::()) - .and(warp::body::json()) .and(warp::path("feerecipient")) + .and(warp::body::json()) .and(warp::path::end()) .and(validator_store_filter.clone()) .and(signer.clone()) @@ -700,6 +700,115 @@ pub fn serve( ) .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()) + .and(signer.clone()) + .and_then( + |validator_pubkey: PublicKey, validator_store: Arc>, signer| { + blocking_signed_json_task(signer, 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()) + .and(signer.clone()) + .and_then( + |validator_pubkey: PublicKey, + request: api_types::UpdateGasLimitRequest, + validator_store: Arc>, + signer| { + blocking_signed_json_task(signer, 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()) + .and(signer.clone()) + .and_then( + |validator_pubkey: PublicKey, validator_store: Arc>, signer| { + blocking_signed_json_task(signer, 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)); + // GET /eth/v1/keystores let get_std_keystores = std_keystores .and(signer.clone()) @@ -786,6 +895,7 @@ pub fn serve( .or(get_lighthouse_validators) .or(get_lighthouse_validators_pubkey) .or(get_fee_recipient) + .or(get_gas_limit) .or(get_std_keystores) .or(get_std_remotekeys), ) @@ -795,12 +905,14 @@ pub fn serve( .or(post_validators_mnemonic) .or(post_validators_web3signer) .or(post_fee_recipient) + .or(post_gas_limit) .or(post_std_keystores) .or(post_std_remotekeys), )) .or(warp::patch().and(patch_validators)) .or(warp::delete().and( delete_fee_recipient + .or(delete_gas_limit) .or(delete_std_keystores) .or(delete_std_remotekeys), )), diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/src/http_api/tests/keystores.rs index c3b5f0bb9..5cc755db5 100644 --- a/validator_client/src/http_api/tests/keystores.rs +++ b/validator_client/src/http_api/tests/keystores.rs @@ -1,3 +1,4 @@ +use super::super::super::validator_store::DEFAULT_GAS_LIMIT; use super::*; use account_utils::random_password_string; use bls::PublicKeyBytes; @@ -769,6 +770,181 @@ fn check_get_set_fee_recipient() { }) } +#[test] +fn check_get_set_gas_limit() { + run_test(|tester: ApiTester| async move { + let _ = &tester; + let password = random_password_string(); + let keystores = (0..3) + .map(|_| new_keystore(password.clone())) + .collect::>(); + let all_pubkeys = keystores.iter().map(keystore_pubkey).collect::>(); + + let import_res = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }) + .await + .unwrap(); + + // All keystores should be imported. + check_keystore_import_response(&import_res, all_imported(keystores.len())); + + // Check that GET lists all the imported keystores. + let get_res = tester.client.get_keystores().await.unwrap(); + check_keystore_get_response(&get_res, &keystores); + + // Before setting anything, every gas limit should be set to DEFAULT_GAS_LIMIT + for pubkey in &all_pubkeys { + let get_res = tester + .client + .get_gas_limit(pubkey) + .await + .expect("should get gas limit"); + assert_eq!( + get_res, + GetGasLimitResponse { + pubkey: pubkey.clone(), + gas_limit: DEFAULT_GAS_LIMIT, + } + ); + } + + let gas_limit_public_key_1 = 40_000_000; + let gas_limit_public_key_2 = 42; + let gas_limit_override = 100; + + // set the gas limit for pubkey[1] using the API + tester + .client + .post_gas_limit( + &all_pubkeys[1], + &UpdateGasLimitRequest { + gas_limit: gas_limit_public_key_1, + }, + ) + .await + .expect("should update gas limit"); + // now everything but pubkey[1] should be DEFAULT_GAS_LIMIT + for (i, pubkey) in all_pubkeys.iter().enumerate() { + let get_res = tester + .client + .get_gas_limit(pubkey) + .await + .expect("should get gas limit"); + let expected = if i == 1 { + gas_limit_public_key_1.clone() + } else { + DEFAULT_GAS_LIMIT + }; + assert_eq!( + get_res, + GetGasLimitResponse { + pubkey: pubkey.clone(), + gas_limit: expected, + } + ); + } + + // set the gas limit for pubkey[2] using the API + tester + .client + .post_gas_limit( + &all_pubkeys[2], + &UpdateGasLimitRequest { + gas_limit: gas_limit_public_key_2, + }, + ) + .await + .expect("should update gas limit"); + // now everything but pubkey[1] & pubkey[2] should be DEFAULT_GAS_LIMIT + for (i, pubkey) in all_pubkeys.iter().enumerate() { + let get_res = tester + .client + .get_gas_limit(pubkey) + .await + .expect("should get gas limit"); + let expected = if i == 1 { + gas_limit_public_key_1 + } else if i == 2 { + gas_limit_public_key_2 + } else { + DEFAULT_GAS_LIMIT + }; + assert_eq!( + get_res, + GetGasLimitResponse { + pubkey: pubkey.clone(), + gas_limit: expected, + } + ); + } + + // should be able to override previous gas_limit + tester + .client + .post_gas_limit( + &all_pubkeys[1], + &UpdateGasLimitRequest { + gas_limit: gas_limit_override, + }, + ) + .await + .expect("should update gas limit"); + for (i, pubkey) in all_pubkeys.iter().enumerate() { + let get_res = tester + .client + .get_gas_limit(pubkey) + .await + .expect("should get gas limit"); + let expected = if i == 1 { + gas_limit_override + } else if i == 2 { + gas_limit_public_key_2 + } else { + DEFAULT_GAS_LIMIT + }; + assert_eq!( + get_res, + GetGasLimitResponse { + pubkey: pubkey.clone(), + gas_limit: expected, + } + ); + } + + // delete gas limit for pubkey[1] using the API + tester + .client + .delete_gas_limit(&all_pubkeys[1]) + .await + .expect("should delete gas limit"); + // now everything but pubkey[2] should be DEFAULT_GAS_LIMIT + for (i, pubkey) in all_pubkeys.iter().enumerate() { + let get_res = tester + .client + .get_gas_limit(pubkey) + .await + .expect("should get gas limit"); + let expected = if i == 2 { + gas_limit_public_key_2 + } else { + DEFAULT_GAS_LIMIT + }; + assert_eq!( + get_res, + GetGasLimitResponse { + pubkey: pubkey.clone(), + gas_limit: expected, + } + ); + } + }) +} + fn all_indices(count: usize) -> Vec { (0..count).collect() } diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 66a621eb7..8d9fbe281 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -795,6 +795,78 @@ impl InitializedValidators { Ok(()) } + /// Sets the `InitializedValidator` and `ValidatorDefinition` `gas_limit` values. + /// + /// ## Notes + /// + /// Setting a validator `gas_limit` will cause `self.definitions` to be updated and saved to + /// disk. + /// + /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. + pub fn set_validator_gas_limit( + &mut self, + voting_public_key: &PublicKey, + gas_limit: u64, + ) -> Result<(), Error> { + if let Some(def) = self + .definitions + .as_mut_slice() + .iter_mut() + .find(|def| def.voting_public_key == *voting_public_key) + { + def.gas_limit = Some(gas_limit); + } + + if let Some(val) = self + .validators + .get_mut(&PublicKeyBytes::from(voting_public_key)) + { + val.gas_limit = Some(gas_limit); + } + + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Ok(()) + } + + /// Removes the `InitializedValidator` and `ValidatorDefinition` `gas_limit` values. + /// + /// ## Notes + /// + /// Removing a validator `gas_limit` will cause `self.definitions` to be updated and saved to + /// disk. The gas_limit for the validator will then fall back to the process level default if + /// it is set. + /// + /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. + pub fn delete_validator_gas_limit( + &mut self, + voting_public_key: &PublicKey, + ) -> Result<(), Error> { + if let Some(def) = self + .definitions + .as_mut_slice() + .iter_mut() + .find(|def| def.voting_public_key == *voting_public_key) + { + def.gas_limit = None; + } + + if let Some(val) = self + .validators + .get_mut(&PublicKeyBytes::from(voting_public_key)) + { + val.gas_limit = None; + } + + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Ok(()) + } + /// Tries to decrypt the key cache. /// /// Returns the decrypted cache if decryption was successful, or an error if a required password diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index f883d0201..292b49ac3 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -57,7 +57,7 @@ const SLASHING_PROTECTION_HISTORY_EPOCHS: u64 = 512; /// Currently used as the default gas limit in execution clients. /// /// https://github.com/ethereum/builder-specs/issues/17 -const DEFAULT_GAS_LIMIT: u64 = 30_000_000; +pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; struct LocalValidator { validator_dir: ValidatorDir,