Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Add new validator API for voluntary exit #4119

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions common/eth2/src/lighthouse_vc/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,30 @@ impl ValidatorClientHttpClient {
let url = self.make_gas_limit_url(pubkey)?;
self.delete_with_raw_response(url, &()).await
}

/// `POST /eth/v1/validator/{pubkey}/voluntary_exit`
pub async fn post_validator_voluntary_exit(
&self,
pubkey: &PublicKeyBytes,
epoch: Option<Epoch>,
) -> Result<SignedVoluntaryExit, Error> {
let mut path = self.server.full.clone();

path.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
.push("eth")
.push("v1")
.push("validator")
.push(&pubkey.to_string())
.push("voluntary_exit");

if let Some(epoch) = epoch {
path.query_pairs_mut()
.append_pair("epoch", &epoch.to_string());
}

self.post(path, &()).await
}
}

/// Returns `Ok(response)` if the response is a `200 OK` response or a
Expand Down
5 changes: 5 additions & 0 deletions common/eth2/src/lighthouse_vc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,8 @@ pub struct UpdateGasLimitRequest {
#[serde(with = "eth2_serde_utils::quoted_u64")]
pub gas_limit: u64,
}

#[derive(Deserialize)]
pub struct VoluntaryExitQuery {
pub epoch: Option<Epoch>,
}
55 changes: 55 additions & 0 deletions validator_client/src/http_api/create_signed_voluntary_exit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::validator_store::ValidatorStore;
use bls::{PublicKey, PublicKeyBytes};
use slog::{info, Logger};
use slot_clock::SlotClock;
use std::sync::Arc;
use types::{Epoch, EthSpec, SignedVoluntaryExit, VoluntaryExit};

pub async fn create_signed_voluntary_exit<T: 'static + SlotClock + Clone, E: EthSpec>(
pubkey: PublicKey,
maybe_epoch: Option<Epoch>,
validator_store: Arc<ValidatorStore<T, E>>,
slot_clock: T,
log: Logger,
) -> Result<SignedVoluntaryExit, warp::Rejection> {
let epoch = match maybe_epoch {
Some(epoch) => epoch,
None => get_current_epoch::<T, E>(slot_clock).ok_or_else(|| {
warp_utils::reject::custom_server_error("Unable to determine current epoch".to_string())
})?,
};

let pubkey_bytes = PublicKeyBytes::from(pubkey);
let validator_index = validator_store
.validator_index(&pubkey_bytes)
.ok_or_else(|| {
warp_utils::reject::custom_not_found(format!(
"Unable to find validator with public key: {}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was using this endpoint on Goerli on a VC with 5,000 validators on it (a pretty unreasonable amount of validators, I know). It turns out that it takes some time to obtain all the validator indices for all pubkeys when the VCs starts (on a fresh boot the VC only knows pubkeys but doesn't know validator indices). I was hitting this 404 and it took me a bit to figure out what was going on.

Here's a suggestion you're free to cherry-pick which would clarify things:

b1a1cbf

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @paulhauner, thanks for this, nice changes! 👍 I've cherry-picked your changes. I wasn't aware that the validator indices were fetched / available later - good to know!

pubkey_bytes.as_hex_string()
))
})?;

let voluntary_exit = VoluntaryExit {
epoch,
validator_index,
};

info!(log, "Signing voluntary exit"; "validator" => pubkey_bytes.as_hex_string());
jimmygchen marked this conversation as resolved.
Show resolved Hide resolved

let signed_voluntary_exit = validator_store
.sign_voluntary_exit(pubkey_bytes, voluntary_exit)
.await
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"Failed to sign voluntary exit: {:?}",
e
))
})?;

Ok(signed_voluntary_exit)
}

/// Calculates the current epoch from the genesis time and current time.
fn get_current_epoch<T: 'static + SlotClock + Clone, E: EthSpec>(slot_clock: T) -> Option<Epoch> {
slot_clock.now().map(|s| s.epoch(E::slots_per_epoch()))
}
47 changes: 47 additions & 0 deletions validator_client/src/http_api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod api_secret;
mod create_signed_voluntary_exit;
mod create_validator;
mod keystores;
mod remotekeys;
mod tests;

use crate::http_api::create_signed_voluntary_exit::create_signed_voluntary_exit;
use crate::{determine_graffiti, GraffitiFile, ValidatorStore};
use account_utils::{
mnemonic_from_phrase,
Expand Down Expand Up @@ -71,6 +73,7 @@ pub struct Context<T: SlotClock, E: EthSpec> {
pub spec: ChainSpec,
pub config: Config,
pub log: Logger,
pub slot_clock: T,
pub _phantom: PhantomData<E>,
}

Expand Down Expand Up @@ -189,6 +192,9 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
let inner_ctx = ctx.clone();
let log_filter = warp::any().map(move || inner_ctx.log.clone());

let inner_slot_clock = ctx.slot_clock.clone();
let slot_clock_filter = warp::any().map(move || inner_slot_clock.clone());

let inner_spec = Arc::new(ctx.spec.clone());
let spec_filter = warp::any().map(move || inner_spec.clone());

Expand Down Expand Up @@ -904,6 +910,46 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
)
.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::<PublicKey>())
.and(warp::path("voluntary_exit"))
.and(warp::query::<api_types::VoluntaryExitQuery>())
.and(warp::path::end())
.and(validator_store_filter.clone())
.and(slot_clock_filter)
.and(log_filter.clone())
.and(signer.clone())
.and(task_executor_filter.clone())
.and_then(
|pubkey: PublicKey,
query: api_types::VoluntaryExitQuery,
validator_store: Arc<ValidatorStore<T, E>>,
slot_clock: T,
log,
signer,
task_executor: TaskExecutor| {
blocking_signed_json_task(signer, 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,
log,
))?;
Ok(signed_voluntary_exit)
} else {
Err(warp_utils::reject::custom_server_error(
"Lighthouse shutting down".into(),
))
}
})
},
);

// GET /eth/v1/keystores
let get_std_keystores = std_keystores
.and(signer.clone())
Expand Down Expand Up @@ -1001,6 +1047,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.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)
Expand Down
67 changes: 62 additions & 5 deletions validator_client/src/http_api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ struct ApiTester {
initialized_validators: Arc<RwLock<InitializedValidators>>,
validator_store: Arc<ValidatorStore<TestingSlotClock, E>>,
url: SensitiveUrl,
slot_clock: TestingSlotClock,
_server_shutdown: oneshot::Sender<()>,
_validator_dir: TempDir,
_runtime_shutdown: exit_future::Signal,
Expand Down Expand Up @@ -90,8 +91,12 @@ impl ApiTester {
let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME);
let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap();

let slot_clock =
TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1));
let genesis_time: u64 = 0;
let slot_clock = TestingSlotClock::new(
Slot::new(0),
Duration::from_secs(genesis_time),
Duration::from_secs(1),
);

let (runtime_shutdown, exit) = exit_future::signal();
let (shutdown_tx, _) = futures::channel::mpsc::channel(1);
Expand All @@ -101,9 +106,9 @@ impl ApiTester {
initialized_validators,
slashing_protection,
Hash256::repeat_byte(42),
spec,
spec.clone(),
Some(Arc::new(DoppelgangerService::new(log.clone()))),
slot_clock,
slot_clock.clone(),
&config,
executor.clone(),
log.clone(),
Expand All @@ -129,7 +134,8 @@ impl ApiTester {
listen_port: 0,
allow_origin: None,
},
log,
log: log.clone(),
slot_clock: slot_clock.clone(),
_phantom: PhantomData,
});
let ctx = context.clone();
Expand All @@ -156,6 +162,7 @@ impl ApiTester {
initialized_validators,
validator_store,
url,
slot_clock,
_server_shutdown: shutdown_tx,
_validator_dir: validator_dir,
_runtime_shutdown: runtime_shutdown,
Expand Down Expand Up @@ -494,6 +501,33 @@ impl ApiTester {
self
}

pub async fn test_sign_voluntary_exits(self, index: usize, maybe_epoch: Option<Epoch>) -> Self {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
// manually setting validator index in `ValidatorStore`
self.initialized_validators
.write()
.set_index(&validator.voting_pubkey, 0);

let expected_exit_epoch = maybe_epoch.unwrap_or_else(|| self.get_current_epoch());

let resp = self
.client
.post_validator_voluntary_exit(&validator.voting_pubkey, maybe_epoch)
.await;

assert!(resp.is_ok());
assert_eq!(resp.unwrap().message.epoch, expected_exit_epoch);

self
}

fn get_current_epoch(&self) -> Epoch {
self.slot_clock
.now()
.map(|s| s.epoch(E::slots_per_epoch()))
.unwrap()
}

pub async fn set_validator_enabled(self, index: usize, enabled: bool) -> Self {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];

Expand Down Expand Up @@ -778,6 +812,29 @@ fn hd_validator_creation() {
});
}

#[test]
fn validator_exit() {
let runtime = build_runtime();
let weak_runtime = Arc::downgrade(&runtime);
runtime.block_on(async {
ApiTester::new(weak_runtime)
.await
.create_hd_validators(HdValidatorScenario {
count: 2,
specify_mnemonic: false,
key_derivation_path_offset: 0,
disabled: vec![],
})
.await
.assert_enabled_validators_count(2)
.assert_validators_count(2)
.test_sign_voluntary_exits(0, None)
.await
.test_sign_voluntary_exits(0, Some(Epoch::new(256)))
.await;
});
}

#[test]
fn validator_enabling() {
let runtime = build_runtime();
Expand Down
5 changes: 5 additions & 0 deletions validator_client/src/http_metrics/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ lazy_static::lazy_static! {
"Total count of attempted SyncSelectionProof signings",
&["status"]
);
pub static ref SIGNED_VOLUNTARY_EXITS_TOTAL: Result<IntCounterVec> = try_create_int_counter_vec(
"vc_signed_voluntary_exits_total",
"Total count of VoluntaryExit signings",
&["status"]
);
pub static ref SIGNED_VALIDATOR_REGISTRATIONS_TOTAL: Result<IntCounterVec> = try_create_int_counter_vec(
"builder_validator_registrations_total",
"Total count of ValidatorRegistrationData signings",
Expand Down
5 changes: 4 additions & 1 deletion validator_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub struct ProductionValidatorClient<T: EthSpec> {
doppelganger_service: Option<Arc<DoppelgangerService>>,
preparation_service: PreparationService<SystemTimeSlotClock, T>,
validator_store: Arc<ValidatorStore<SystemTimeSlotClock, T>>,
slot_clock: SystemTimeSlotClock,
http_api_listen_addr: Option<SocketAddr>,
config: Config,
}
Expand Down Expand Up @@ -461,7 +462,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
let sync_committee_service = SyncCommitteeService::new(
duties_service.clone(),
validator_store.clone(),
slot_clock,
slot_clock.clone(),
beacon_nodes.clone(),
context.service_context("sync_committee".into()),
);
Expand All @@ -482,6 +483,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
preparation_service,
validator_store,
config,
slot_clock,
http_api_listen_addr: None,
})
}
Expand Down Expand Up @@ -544,6 +546,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
graffiti_flag: self.config.graffiti,
spec: self.context.eth2_config.spec.clone(),
config: self.config.http_api.clone(),
slot_clock: self.slot_clock.clone(),
log: log.clone(),
_phantom: PhantomData,
});
Expand Down
3 changes: 3 additions & 0 deletions validator_client/src/signing_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub enum SignableMessage<'a, T: EthSpec, Payload: AbstractExecPayload<T> = FullP
},
SignedContributionAndProof(&'a ContributionAndProof<T>),
ValidatorRegistration(&'a ValidatorRegistrationData),
VoluntaryExit(&'a VoluntaryExit),
}

impl<'a, T: EthSpec, Payload: AbstractExecPayload<T>> SignableMessage<'a, T, Payload> {
Expand All @@ -67,6 +68,7 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload<T>> SignableMessage<'a, T, Pay
} => beacon_block_root.signing_root(domain),
SignableMessage::SignedContributionAndProof(c) => c.signing_root(domain),
SignableMessage::ValidatorRegistration(v) => v.signing_root(domain),
SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain),
}
}
}
Expand Down Expand Up @@ -203,6 +205,7 @@ impl SigningMethod {
SignableMessage::ValidatorRegistration(v) => {
Web3SignerObject::ValidatorRegistration(v)
}
SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e),
};

// Determine the Web3Signer message type.
Expand Down
1 change: 0 additions & 1 deletion validator_client/src/signing_method/web3signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ pub enum Web3SignerObject<'a, T: EthSpec, Payload: AbstractExecPayload<T>> {
RandaoReveal {
epoch: Epoch,
},
#[allow(dead_code)]
VoluntaryExit(&'a VoluntaryExit),
SyncCommitteeMessage {
beacon_block_root: Hash256,
Expand Down
Loading