mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-17 03:42:46 +00:00
Split the VC into crates making it more modular (#6453)
* Starting to modularize the VC * Revert changes to eth2 * More progress * More progress * Compiles * Merge latest unstable and make it compile * Fix some lints * Tests compile * Merge latest unstable * Remove unnecessary deps * Merge latest unstable * Correct release tests * Merge latest unstable * Merge remote-tracking branch 'origin/unstable' into modularize-vc * Merge branch 'unstable' into modularize-vc * Revert unnecessary cargo lock changes * Update validator_client/beacon_node_fallback/Cargo.toml * Update validator_client/http_metrics/Cargo.toml * Update validator_client/http_metrics/src/lib.rs * Update validator_client/initialized_validators/Cargo.toml * Update validator_client/signing_method/Cargo.toml * Update validator_client/validator_metrics/Cargo.toml * Update validator_client/validator_services/Cargo.toml * Update validator_client/validator_store/Cargo.toml * Update validator_client/validator_store/src/lib.rs * Merge remote-tracking branch 'origin/unstable' into modularize-vc * Fix format string * Rename doppelganger trait * Don't drop the tempdir * Cargo fmt
This commit is contained in:
@@ -1,729 +0,0 @@
|
||||
use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback};
|
||||
use crate::{
|
||||
duties_service::{DutiesService, DutyAndProof},
|
||||
http_metrics::metrics,
|
||||
validator_store::{Error as ValidatorStoreError, ValidatorStore},
|
||||
};
|
||||
use environment::RuntimeContext;
|
||||
use futures::future::join_all;
|
||||
use slog::{crit, debug, error, info, trace, warn};
|
||||
use slot_clock::SlotClock;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{sleep, sleep_until, Duration, Instant};
|
||||
use tree_hash::TreeHash;
|
||||
use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot};
|
||||
|
||||
/// Builds an `AttestationService`.
|
||||
pub struct AttestationServiceBuilder<T: SlotClock + 'static, E: EthSpec> {
|
||||
duties_service: Option<Arc<DutiesService<T, E>>>,
|
||||
validator_store: Option<Arc<ValidatorStore<T, E>>>,
|
||||
slot_clock: Option<T>,
|
||||
beacon_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
|
||||
context: Option<RuntimeContext<E>>,
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> AttestationServiceBuilder<T, E> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
duties_service: None,
|
||||
validator_store: None,
|
||||
slot_clock: None,
|
||||
beacon_nodes: None,
|
||||
context: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duties_service(mut self, service: Arc<DutiesService<T, E>>) -> Self {
|
||||
self.duties_service = Some(service);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validator_store(mut self, store: Arc<ValidatorStore<T, E>>) -> Self {
|
||||
self.validator_store = Some(store);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn slot_clock(mut self, slot_clock: T) -> Self {
|
||||
self.slot_clock = Some(slot_clock);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn beacon_nodes(mut self, beacon_nodes: Arc<BeaconNodeFallback<T, E>>) -> Self {
|
||||
self.beacon_nodes = Some(beacon_nodes);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn runtime_context(mut self, context: RuntimeContext<E>) -> Self {
|
||||
self.context = Some(context);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<AttestationService<T, E>, String> {
|
||||
Ok(AttestationService {
|
||||
inner: Arc::new(Inner {
|
||||
duties_service: self
|
||||
.duties_service
|
||||
.ok_or("Cannot build AttestationService without duties_service")?,
|
||||
validator_store: self
|
||||
.validator_store
|
||||
.ok_or("Cannot build AttestationService without validator_store")?,
|
||||
slot_clock: self
|
||||
.slot_clock
|
||||
.ok_or("Cannot build AttestationService without slot_clock")?,
|
||||
beacon_nodes: self
|
||||
.beacon_nodes
|
||||
.ok_or("Cannot build AttestationService without beacon_nodes")?,
|
||||
context: self
|
||||
.context
|
||||
.ok_or("Cannot build AttestationService without runtime_context")?,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to minimise `Arc` usage.
|
||||
pub struct Inner<T, E: EthSpec> {
|
||||
duties_service: Arc<DutiesService<T, E>>,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
slot_clock: T,
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
|
||||
context: RuntimeContext<E>,
|
||||
}
|
||||
|
||||
/// Attempts to produce attestations for all known validators 1/3rd of the way through each slot.
|
||||
///
|
||||
/// If any validators are on the same committee, a single attestation will be downloaded and
|
||||
/// returned to the beacon node. This attestation will have a signature from each of the
|
||||
/// validators.
|
||||
pub struct AttestationService<T, E: EthSpec> {
|
||||
inner: Arc<Inner<T, E>>,
|
||||
}
|
||||
|
||||
impl<T, E: EthSpec> Clone for AttestationService<T, E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E: EthSpec> Deref for AttestationService<T, E> {
|
||||
type Target = Inner<T, E>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> {
|
||||
/// Starts the service which periodically produces attestations.
|
||||
pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> {
|
||||
let log = self.context.log().clone();
|
||||
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
let duration_to_next_slot = self
|
||||
.slot_clock
|
||||
.duration_to_next_slot()
|
||||
.ok_or("Unable to determine duration to next slot")?;
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Attestation production service started";
|
||||
"next_update_millis" => duration_to_next_slot.as_millis()
|
||||
);
|
||||
|
||||
let executor = self.context.executor.clone();
|
||||
|
||||
let interval_fut = async move {
|
||||
loop {
|
||||
if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() {
|
||||
sleep(duration_to_next_slot + slot_duration / 3).await;
|
||||
let log = self.context.log();
|
||||
|
||||
if let Err(e) = self.spawn_attestation_tasks(slot_duration) {
|
||||
crit!(
|
||||
log,
|
||||
"Failed to spawn attestation tasks";
|
||||
"error" => e
|
||||
)
|
||||
} else {
|
||||
trace!(
|
||||
log,
|
||||
"Spawned attestation tasks";
|
||||
)
|
||||
}
|
||||
} else {
|
||||
error!(log, "Failed to read slot clock");
|
||||
// If we can't read the slot clock, just wait another slot.
|
||||
sleep(slot_duration).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
executor.spawn(interval_fut, "attestation_service");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For each each required attestation, spawn a new task that downloads, signs and uploads the
|
||||
/// attestation to the beacon node.
|
||||
fn spawn_attestation_tasks(&self, slot_duration: Duration) -> Result<(), String> {
|
||||
let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?;
|
||||
let duration_to_next_slot = self
|
||||
.slot_clock
|
||||
.duration_to_next_slot()
|
||||
.ok_or("Unable to determine duration to next slot")?;
|
||||
|
||||
// If a validator needs to publish an aggregate attestation, they must do so at 2/3
|
||||
// through the slot. This delay triggers at this time
|
||||
let aggregate_production_instant = Instant::now()
|
||||
+ duration_to_next_slot
|
||||
.checked_sub(slot_duration / 3)
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
let duties_by_committee_index: HashMap<CommitteeIndex, Vec<DutyAndProof>> = self
|
||||
.duties_service
|
||||
.attesters(slot)
|
||||
.into_iter()
|
||||
.fold(HashMap::new(), |mut map, duty_and_proof| {
|
||||
map.entry(duty_and_proof.duty.committee_index)
|
||||
.or_default()
|
||||
.push(duty_and_proof);
|
||||
map
|
||||
});
|
||||
|
||||
// For each committee index for this slot:
|
||||
//
|
||||
// - Create and publish an `Attestation` for all required validators.
|
||||
// - Create and publish `SignedAggregateAndProof` for all aggregating validators.
|
||||
duties_by_committee_index
|
||||
.into_iter()
|
||||
.for_each(|(committee_index, validator_duties)| {
|
||||
// Spawn a separate task for each attestation.
|
||||
self.inner.context.executor.spawn_ignoring_error(
|
||||
self.clone().publish_attestations_and_aggregates(
|
||||
slot,
|
||||
committee_index,
|
||||
validator_duties,
|
||||
aggregate_production_instant,
|
||||
),
|
||||
"attestation publish",
|
||||
);
|
||||
});
|
||||
|
||||
// Schedule pruning of the slashing protection database once all unaggregated
|
||||
// attestations have (hopefully) been signed, i.e. at the same time as aggregate
|
||||
// production.
|
||||
self.spawn_slashing_protection_pruning_task(slot, aggregate_production_instant);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs the first step of the attesting process: downloading `Attestation` objects,
|
||||
/// signing them and returning them to the validator.
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#attesting
|
||||
///
|
||||
/// ## Detail
|
||||
///
|
||||
/// The given `validator_duties` should already be filtered to only contain those that match
|
||||
/// `slot` and `committee_index`. Critical errors will be logged if this is not the case.
|
||||
async fn publish_attestations_and_aggregates(
|
||||
self,
|
||||
slot: Slot,
|
||||
committee_index: CommitteeIndex,
|
||||
validator_duties: Vec<DutyAndProof>,
|
||||
aggregate_production_instant: Instant,
|
||||
) -> Result<(), ()> {
|
||||
let log = self.context.log();
|
||||
let attestations_timer = metrics::start_timer_vec(
|
||||
&metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[metrics::ATTESTATIONS],
|
||||
);
|
||||
|
||||
// There's not need to produce `Attestation` or `SignedAggregateAndProof` if we do not have
|
||||
// any validators for the given `slot` and `committee_index`.
|
||||
if validator_duties.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Step 1.
|
||||
//
|
||||
// Download, sign and publish an `Attestation` for each validator.
|
||||
let attestation_opt = self
|
||||
.produce_and_publish_attestations(slot, committee_index, &validator_duties)
|
||||
.await
|
||||
.map_err(move |e| {
|
||||
crit!(
|
||||
log,
|
||||
"Error during attestation routine";
|
||||
"error" => format!("{:?}", e),
|
||||
"committee_index" => committee_index,
|
||||
"slot" => slot.as_u64(),
|
||||
)
|
||||
})?;
|
||||
|
||||
drop(attestations_timer);
|
||||
|
||||
// Step 2.
|
||||
//
|
||||
// If an attestation was produced, make an aggregate.
|
||||
if let Some(attestation_data) = attestation_opt {
|
||||
// First, wait until the `aggregation_production_instant` (2/3rds
|
||||
// of the way though the slot). As verified in the
|
||||
// `delay_triggers_when_in_the_past` test, this code will still run
|
||||
// even if the instant has already elapsed.
|
||||
sleep_until(aggregate_production_instant).await;
|
||||
|
||||
// Start the metrics timer *after* we've done the delay.
|
||||
let _aggregates_timer = metrics::start_timer_vec(
|
||||
&metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[metrics::AGGREGATES],
|
||||
);
|
||||
|
||||
// Then download, sign and publish a `SignedAggregateAndProof` for each
|
||||
// validator that is elected to aggregate for this `slot` and
|
||||
// `committee_index`.
|
||||
self.produce_and_publish_aggregates(
|
||||
&attestation_data,
|
||||
committee_index,
|
||||
&validator_duties,
|
||||
)
|
||||
.await
|
||||
.map_err(move |e| {
|
||||
crit!(
|
||||
log,
|
||||
"Error during attestation routine";
|
||||
"error" => format!("{:?}", e),
|
||||
"committee_index" => committee_index,
|
||||
"slot" => slot.as_u64(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs the first step of the attesting process: downloading `Attestation` objects,
|
||||
/// signing them and returning them to the validator.
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#attesting
|
||||
///
|
||||
/// ## Detail
|
||||
///
|
||||
/// The given `validator_duties` should already be filtered to only contain those that match
|
||||
/// `slot` and `committee_index`. Critical errors will be logged if this is not the case.
|
||||
///
|
||||
/// Only one `Attestation` is downloaded from the BN. It is then cloned and signed by each
|
||||
/// validator and the list of individually-signed `Attestation` objects is returned to the BN.
|
||||
async fn produce_and_publish_attestations(
|
||||
&self,
|
||||
slot: Slot,
|
||||
committee_index: CommitteeIndex,
|
||||
validator_duties: &[DutyAndProof],
|
||||
) -> Result<Option<AttestationData>, String> {
|
||||
let log = self.context.log();
|
||||
|
||||
if validator_duties.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let current_epoch = self
|
||||
.slot_clock
|
||||
.now()
|
||||
.ok_or("Unable to determine current slot from clock")?
|
||||
.epoch(E::slots_per_epoch());
|
||||
|
||||
let attestation_data = self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[metrics::ATTESTATIONS_HTTP_GET],
|
||||
);
|
||||
beacon_node
|
||||
.get_validator_attestation_data(slot, committee_index)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to produce attestation data: {:?}", e))
|
||||
.map(|result| result.data)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Create futures to produce signed `Attestation` objects.
|
||||
let attestation_data_ref = &attestation_data;
|
||||
let signing_futures = validator_duties.iter().map(|duty_and_proof| async move {
|
||||
let duty = &duty_and_proof.duty;
|
||||
let attestation_data = attestation_data_ref;
|
||||
|
||||
// Ensure that the attestation matches the duties.
|
||||
if !duty.match_attestation_data::<E>(attestation_data, &self.context.eth2_config.spec) {
|
||||
crit!(
|
||||
log,
|
||||
"Inconsistent validator duties during signing";
|
||||
"validator" => ?duty.pubkey,
|
||||
"duty_slot" => duty.slot,
|
||||
"attestation_slot" => attestation_data.slot,
|
||||
"duty_index" => duty.committee_index,
|
||||
"attestation_index" => attestation_data.index,
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut attestation = match Attestation::<E>::empty_for_signing(
|
||||
duty.committee_index,
|
||||
duty.committee_length as usize,
|
||||
attestation_data.slot,
|
||||
attestation_data.beacon_block_root,
|
||||
attestation_data.source,
|
||||
attestation_data.target,
|
||||
&self.context.eth2_config.spec,
|
||||
) {
|
||||
Ok(attestation) => attestation,
|
||||
Err(err) => {
|
||||
crit!(
|
||||
log,
|
||||
"Invalid validator duties during signing";
|
||||
"validator" => ?duty.pubkey,
|
||||
"duty" => ?duty,
|
||||
"err" => ?err,
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.validator_store
|
||||
.sign_attestation(
|
||||
duty.pubkey,
|
||||
duty.validator_committee_index as usize,
|
||||
&mut attestation,
|
||||
current_epoch,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Some((attestation, duty.validator_index)),
|
||||
Err(ValidatorStoreError::UnknownPubkey(pubkey)) => {
|
||||
// A pubkey can be missing when a validator was recently
|
||||
// removed via the API.
|
||||
warn!(
|
||||
log,
|
||||
"Missing pubkey for attestation";
|
||||
"info" => "a validator may have recently been removed from this VC",
|
||||
"pubkey" => ?pubkey,
|
||||
"validator" => ?duty.pubkey,
|
||||
"committee_index" => committee_index,
|
||||
"slot" => slot.as_u64(),
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
log,
|
||||
"Failed to sign attestation";
|
||||
"error" => ?e,
|
||||
"validator" => ?duty.pubkey,
|
||||
"committee_index" => committee_index,
|
||||
"slot" => slot.as_u64(),
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let (ref attestations, ref validator_indices): (Vec<_>, Vec<_>) = join_all(signing_futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.unzip();
|
||||
|
||||
if attestations.is_empty() {
|
||||
warn!(log, "No attestations were published");
|
||||
return Ok(None);
|
||||
}
|
||||
let fork_name = self
|
||||
.context
|
||||
.eth2_config
|
||||
.spec
|
||||
.fork_name_at_slot::<E>(attestation_data.slot);
|
||||
|
||||
// Post the attestations to the BN.
|
||||
match self
|
||||
.beacon_nodes
|
||||
.request(ApiTopic::Attestations, |beacon_node| async move {
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[metrics::ATTESTATIONS_HTTP_POST],
|
||||
);
|
||||
if fork_name.electra_enabled() {
|
||||
beacon_node
|
||||
.post_beacon_pool_attestations_v2(attestations, fork_name)
|
||||
.await
|
||||
} else {
|
||||
beacon_node
|
||||
.post_beacon_pool_attestations_v1(attestations)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => info!(
|
||||
log,
|
||||
"Successfully published attestations";
|
||||
"count" => attestations.len(),
|
||||
"validator_indices" => ?validator_indices,
|
||||
"head_block" => ?attestation_data.beacon_block_root,
|
||||
"committee_index" => attestation_data.index,
|
||||
"slot" => attestation_data.slot.as_u64(),
|
||||
"type" => "unaggregated",
|
||||
),
|
||||
Err(e) => error!(
|
||||
log,
|
||||
"Unable to publish attestations";
|
||||
"error" => %e,
|
||||
"committee_index" => attestation_data.index,
|
||||
"slot" => slot.as_u64(),
|
||||
"type" => "unaggregated",
|
||||
),
|
||||
}
|
||||
|
||||
Ok(Some(attestation_data))
|
||||
}
|
||||
|
||||
/// Performs the second step of the attesting process: downloading an aggregated `Attestation`,
|
||||
/// converting it into a `SignedAggregateAndProof` and returning it to the BN.
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#broadcast-aggregate
|
||||
///
|
||||
/// ## Detail
|
||||
///
|
||||
/// The given `validator_duties` should already be filtered to only contain those that match
|
||||
/// `slot` and `committee_index`. Critical errors will be logged if this is not the case.
|
||||
///
|
||||
/// Only one aggregated `Attestation` is downloaded from the BN. It is then cloned and signed
|
||||
/// by each validator and the list of individually-signed `SignedAggregateAndProof` objects is
|
||||
/// returned to the BN.
|
||||
async fn produce_and_publish_aggregates(
|
||||
&self,
|
||||
attestation_data: &AttestationData,
|
||||
committee_index: CommitteeIndex,
|
||||
validator_duties: &[DutyAndProof],
|
||||
) -> Result<(), String> {
|
||||
let log = self.context.log();
|
||||
|
||||
if !validator_duties
|
||||
.iter()
|
||||
.any(|duty_and_proof| duty_and_proof.selection_proof.is_some())
|
||||
{
|
||||
// Exit early if no validator is aggregator
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let fork_name = self
|
||||
.context
|
||||
.eth2_config
|
||||
.spec
|
||||
.fork_name_at_slot::<E>(attestation_data.slot);
|
||||
|
||||
let aggregated_attestation = &self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[metrics::AGGREGATES_HTTP_GET],
|
||||
);
|
||||
if fork_name.electra_enabled() {
|
||||
beacon_node
|
||||
.get_validator_aggregate_attestation_v2(
|
||||
attestation_data.slot,
|
||||
attestation_data.tree_hash_root(),
|
||||
committee_index,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
format!("Failed to produce an aggregate attestation: {:?}", e)
|
||||
})?
|
||||
.ok_or_else(|| format!("No aggregate available for {:?}", attestation_data))
|
||||
.map(|result| result.data)
|
||||
} else {
|
||||
beacon_node
|
||||
.get_validator_aggregate_attestation_v1(
|
||||
attestation_data.slot,
|
||||
attestation_data.tree_hash_root(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
format!("Failed to produce an aggregate attestation: {:?}", e)
|
||||
})?
|
||||
.ok_or_else(|| format!("No aggregate available for {:?}", attestation_data))
|
||||
.map(|result| result.data)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Create futures to produce the signed aggregated attestations.
|
||||
let signing_futures = validator_duties.iter().map(|duty_and_proof| async move {
|
||||
let duty = &duty_and_proof.duty;
|
||||
let selection_proof = duty_and_proof.selection_proof.as_ref()?;
|
||||
|
||||
if !duty.match_attestation_data::<E>(attestation_data, &self.context.eth2_config.spec) {
|
||||
crit!(log, "Inconsistent validator duties during signing");
|
||||
return None;
|
||||
}
|
||||
|
||||
match self
|
||||
.validator_store
|
||||
.produce_signed_aggregate_and_proof(
|
||||
duty.pubkey,
|
||||
duty.validator_index,
|
||||
aggregated_attestation.clone(),
|
||||
selection_proof.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(aggregate) => Some(aggregate),
|
||||
Err(ValidatorStoreError::UnknownPubkey(pubkey)) => {
|
||||
// A pubkey can be missing when a validator was recently
|
||||
// removed via the API.
|
||||
debug!(
|
||||
log,
|
||||
"Missing pubkey for aggregate";
|
||||
"pubkey" => ?pubkey,
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
log,
|
||||
"Failed to sign aggregate";
|
||||
"error" => ?e,
|
||||
"pubkey" => ?duty.pubkey,
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let signed_aggregate_and_proofs = join_all(signing_futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !signed_aggregate_and_proofs.is_empty() {
|
||||
let signed_aggregate_and_proofs_slice = signed_aggregate_and_proofs.as_slice();
|
||||
match self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[metrics::AGGREGATES_HTTP_POST],
|
||||
);
|
||||
if fork_name.electra_enabled() {
|
||||
beacon_node
|
||||
.post_validator_aggregate_and_proof_v2(
|
||||
signed_aggregate_and_proofs_slice,
|
||||
fork_name,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
beacon_node
|
||||
.post_validator_aggregate_and_proof_v1(
|
||||
signed_aggregate_and_proofs_slice,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
for signed_aggregate_and_proof in signed_aggregate_and_proofs {
|
||||
let attestation = signed_aggregate_and_proof.message().aggregate();
|
||||
info!(
|
||||
log,
|
||||
"Successfully published attestation";
|
||||
"aggregator" => signed_aggregate_and_proof.message().aggregator_index(),
|
||||
"signatures" => attestation.num_set_aggregation_bits(),
|
||||
"head_block" => format!("{:?}", attestation.data().beacon_block_root),
|
||||
"committee_index" => attestation.committee_index(),
|
||||
"slot" => attestation.data().slot.as_u64(),
|
||||
"type" => "aggregated",
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
for signed_aggregate_and_proof in signed_aggregate_and_proofs {
|
||||
let attestation = &signed_aggregate_and_proof.message().aggregate();
|
||||
crit!(
|
||||
log,
|
||||
"Failed to publish attestation";
|
||||
"error" => %e,
|
||||
"aggregator" => signed_aggregate_and_proof.message().aggregator_index(),
|
||||
"committee_index" => attestation.committee_index(),
|
||||
"slot" => attestation.data().slot.as_u64(),
|
||||
"type" => "aggregated",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spawn a blocking task to run the slashing protection pruning process.
|
||||
///
|
||||
/// Start the task at `pruning_instant` to avoid interference with other tasks.
|
||||
fn spawn_slashing_protection_pruning_task(&self, slot: Slot, pruning_instant: Instant) {
|
||||
let attestation_service = self.clone();
|
||||
let executor = self.inner.context.executor.clone();
|
||||
let current_epoch = slot.epoch(E::slots_per_epoch());
|
||||
|
||||
// Wait for `pruning_instant` in a regular task, and then switch to a blocking one.
|
||||
self.inner.context.executor.spawn(
|
||||
async move {
|
||||
sleep_until(pruning_instant).await;
|
||||
|
||||
executor.spawn_blocking(
|
||||
move || {
|
||||
attestation_service
|
||||
.validator_store
|
||||
.prune_slashing_protection_db(current_epoch, false)
|
||||
},
|
||||
"slashing_protection_pruning",
|
||||
)
|
||||
},
|
||||
"slashing_protection_pre_pruning",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::future::FutureExt;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
/// This test is to ensure that a `tokio_timer::Sleep` with an instant in the past will still
|
||||
/// trigger.
|
||||
#[tokio::test]
|
||||
async fn delay_triggers_when_in_the_past() {
|
||||
let in_the_past = Instant::now() - Duration::from_secs(2);
|
||||
let state_1 = Arc::new(RwLock::new(in_the_past));
|
||||
let state_2 = state_1.clone();
|
||||
|
||||
sleep_until(in_the_past)
|
||||
.map(move |()| *state_1.write() = Instant::now())
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
*state_2.read() > in_the_past,
|
||||
"state should have been updated"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,870 +0,0 @@
|
||||
//! Allows for a list of `BeaconNodeHttpClient` to appear as a single entity which will exhibits
|
||||
//! "fallback" behaviour; it will try a request on all of the nodes until one or none of them
|
||||
//! succeed.
|
||||
|
||||
use crate::beacon_node_health::{
|
||||
BeaconNodeHealth, BeaconNodeSyncDistanceTiers, ExecutionEngineHealth, IsOptimistic,
|
||||
SyncDistanceTier,
|
||||
};
|
||||
use crate::check_synced::check_node_health;
|
||||
use crate::http_metrics::metrics::{inc_counter_vec, ENDPOINT_ERRORS, ENDPOINT_REQUESTS};
|
||||
use environment::RuntimeContext;
|
||||
use eth2::BeaconNodeHttpClient;
|
||||
use futures::future;
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
|
||||
use slog::{debug, error, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use strum::{EnumString, EnumVariantNames};
|
||||
use tokio::{sync::RwLock, time::sleep};
|
||||
use types::{ChainSpec, Config as ConfigSpec, EthSpec, Slot};
|
||||
|
||||
/// Message emitted when the VC detects the BN is using a different spec.
|
||||
const UPDATE_REQUIRED_LOG_HINT: &str = "this VC or the remote BN may need updating";
|
||||
|
||||
/// The number of seconds *prior* to slot start that we will try and update the state of fallback
|
||||
/// nodes.
|
||||
///
|
||||
/// Ideally this should be somewhere between 2/3rds through the slot and the end of it. If we set it
|
||||
/// too early, we risk switching nodes between the time of publishing an attestation and publishing
|
||||
/// an aggregate; this may result in a missed aggregation. If we set this time too late, we risk not
|
||||
/// having the correct nodes up and running prior to the start of the slot.
|
||||
const SLOT_LOOKAHEAD: Duration = Duration::from_secs(2);
|
||||
|
||||
/// If the beacon node slot_clock is within 1 slot, this is deemed acceptable. Otherwise the node
|
||||
/// will be marked as CandidateError::TimeDiscrepancy.
|
||||
const FUTURE_SLOT_TOLERANCE: Slot = Slot::new(1);
|
||||
|
||||
// Configuration for the Beacon Node fallback.
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub sync_tolerances: BeaconNodeSyncDistanceTiers,
|
||||
}
|
||||
|
||||
/// Indicates a measurement of latency between the VC and a BN.
|
||||
pub struct LatencyMeasurement {
|
||||
/// An identifier for the beacon node (e.g. the URL).
|
||||
pub beacon_node_id: String,
|
||||
/// The round-trip latency, if the BN responded successfully.
|
||||
pub latency: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Starts a service that will routinely try and update the status of the provided `beacon_nodes`.
|
||||
///
|
||||
/// See `SLOT_LOOKAHEAD` for information about when this should run.
|
||||
pub fn start_fallback_updater_service<T: SlotClock + 'static, E: EthSpec>(
|
||||
context: RuntimeContext<E>,
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
|
||||
) -> Result<(), &'static str> {
|
||||
let executor = context.executor;
|
||||
if beacon_nodes.slot_clock.is_none() {
|
||||
return Err("Cannot start fallback updater without slot clock");
|
||||
}
|
||||
|
||||
let future = async move {
|
||||
loop {
|
||||
beacon_nodes.update_all_candidates().await;
|
||||
|
||||
let sleep_time = beacon_nodes
|
||||
.slot_clock
|
||||
.as_ref()
|
||||
.and_then(|slot_clock| {
|
||||
let slot = slot_clock.now()?;
|
||||
let till_next_slot = slot_clock.duration_to_slot(slot + 1)?;
|
||||
|
||||
till_next_slot.checked_sub(SLOT_LOOKAHEAD)
|
||||
})
|
||||
.unwrap_or_else(|| Duration::from_secs(1));
|
||||
|
||||
sleep(sleep_time).await
|
||||
}
|
||||
};
|
||||
|
||||
executor.spawn(future, "fallback");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error<T> {
|
||||
/// We attempted to contact the node but it failed.
|
||||
RequestFailed(T),
|
||||
}
|
||||
|
||||
impl<T> Error<T> {
|
||||
pub fn request_failure(&self) -> Option<&T> {
|
||||
match self {
|
||||
Error::RequestFailed(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of errors encountered whilst attempting to perform a query.
|
||||
pub struct Errors<T>(pub Vec<(String, Error<T>)>);
|
||||
|
||||
impl<T: Debug> fmt::Display for Errors<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if !self.0.is_empty() {
|
||||
write!(f, "Some endpoints failed, num_failed: {}", self.0.len())?;
|
||||
}
|
||||
for (i, (id, error)) in self.0.iter().enumerate() {
|
||||
let comma = if i + 1 < self.0.len() { "," } else { "" };
|
||||
|
||||
write!(f, " {} => {:?}{}", id, error, comma)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Errors<T> {
|
||||
pub fn num_errors(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasons why a candidate might not be ready.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
|
||||
pub enum CandidateError {
|
||||
PreGenesis,
|
||||
Uninitialized,
|
||||
Offline,
|
||||
Incompatible,
|
||||
TimeDiscrepancy,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CandidateError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CandidateError::PreGenesis => write!(f, "PreGenesis"),
|
||||
CandidateError::Uninitialized => write!(f, "Uninitialized"),
|
||||
CandidateError::Offline => write!(f, "Offline"),
|
||||
CandidateError::Incompatible => write!(f, "Incompatible"),
|
||||
CandidateError::TimeDiscrepancy => write!(f, "TimeDiscrepancy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CandidateInfo {
|
||||
pub index: usize,
|
||||
pub endpoint: String,
|
||||
pub health: Result<BeaconNodeHealth, CandidateError>,
|
||||
}
|
||||
|
||||
impl Serialize for CandidateInfo {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("CandidateInfo", 2)?;
|
||||
|
||||
state.serialize_field("index", &self.index)?;
|
||||
state.serialize_field("endpoint", &self.endpoint)?;
|
||||
|
||||
// Serialize either the health or the error field based on the Result
|
||||
match &self.health {
|
||||
Ok(health) => {
|
||||
state.serialize_field("health", health)?;
|
||||
}
|
||||
Err(e) => {
|
||||
state.serialize_field("error", &e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a `BeaconNodeHttpClient` inside a `BeaconNodeFallback` that may or may not be used
|
||||
/// for a query.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CandidateBeaconNode<E> {
|
||||
pub index: usize,
|
||||
pub beacon_node: BeaconNodeHttpClient,
|
||||
pub health: Arc<RwLock<Result<BeaconNodeHealth, CandidateError>>>,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> PartialEq for CandidateBeaconNode<E> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index && self.beacon_node == other.beacon_node
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: EthSpec> Eq for CandidateBeaconNode<E> {}
|
||||
|
||||
impl<E: EthSpec> CandidateBeaconNode<E> {
|
||||
/// Instantiate a new node.
|
||||
pub fn new(beacon_node: BeaconNodeHttpClient, index: usize) -> Self {
|
||||
Self {
|
||||
index,
|
||||
beacon_node,
|
||||
health: Arc::new(RwLock::new(Err(CandidateError::Uninitialized))),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the health of `self`.
|
||||
pub async fn health(&self) -> Result<BeaconNodeHealth, CandidateError> {
|
||||
*self.health.read().await
|
||||
}
|
||||
|
||||
pub async fn refresh_health<T: SlotClock>(
|
||||
&self,
|
||||
distance_tiers: &BeaconNodeSyncDistanceTiers,
|
||||
slot_clock: Option<&T>,
|
||||
spec: &ChainSpec,
|
||||
log: &Logger,
|
||||
) -> Result<(), CandidateError> {
|
||||
if let Err(e) = self.is_compatible(spec, log).await {
|
||||
*self.health.write().await = Err(e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
if let Some(slot_clock) = slot_clock {
|
||||
match check_node_health(&self.beacon_node, log).await {
|
||||
Ok((head, is_optimistic, el_offline)) => {
|
||||
let Some(slot_clock_head) = slot_clock.now() else {
|
||||
let e = match slot_clock.is_prior_to_genesis() {
|
||||
Some(true) => CandidateError::PreGenesis,
|
||||
_ => CandidateError::Uninitialized,
|
||||
};
|
||||
*self.health.write().await = Err(e);
|
||||
return Err(e);
|
||||
};
|
||||
|
||||
if head > slot_clock_head + FUTURE_SLOT_TOLERANCE {
|
||||
let e = CandidateError::TimeDiscrepancy;
|
||||
*self.health.write().await = Err(e);
|
||||
return Err(e);
|
||||
}
|
||||
let sync_distance = slot_clock_head.saturating_sub(head);
|
||||
|
||||
// Currently ExecutionEngineHealth is solely determined by online status.
|
||||
let execution_status = if el_offline {
|
||||
ExecutionEngineHealth::Unhealthy
|
||||
} else {
|
||||
ExecutionEngineHealth::Healthy
|
||||
};
|
||||
|
||||
let optimistic_status = if is_optimistic {
|
||||
IsOptimistic::Yes
|
||||
} else {
|
||||
IsOptimistic::No
|
||||
};
|
||||
|
||||
let new_health = BeaconNodeHealth::from_status(
|
||||
self.index,
|
||||
sync_distance,
|
||||
head,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
distance_tiers,
|
||||
);
|
||||
|
||||
*self.health.write().await = Ok(new_health);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
// Set the health as `Err` which is sorted last in the list.
|
||||
*self.health.write().await = Err(e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Slot clock will only be `None` at startup.
|
||||
let e = CandidateError::Uninitialized;
|
||||
*self.health.write().await = Err(e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the node has the correct specification.
|
||||
async fn is_compatible(&self, spec: &ChainSpec, log: &Logger) -> Result<(), CandidateError> {
|
||||
let config = self
|
||||
.beacon_node
|
||||
.get_config_spec::<ConfigSpec>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
log,
|
||||
"Unable to read spec from beacon node";
|
||||
"error" => %e,
|
||||
"endpoint" => %self.beacon_node,
|
||||
);
|
||||
CandidateError::Offline
|
||||
})?
|
||||
.data;
|
||||
|
||||
let beacon_node_spec = ChainSpec::from_config::<E>(&config).ok_or_else(|| {
|
||||
error!(
|
||||
log,
|
||||
"The minimal/mainnet spec type of the beacon node does not match the validator \
|
||||
client. See the --network command.";
|
||||
"endpoint" => %self.beacon_node,
|
||||
);
|
||||
CandidateError::Incompatible
|
||||
})?;
|
||||
|
||||
if beacon_node_spec.genesis_fork_version != spec.genesis_fork_version {
|
||||
error!(
|
||||
log,
|
||||
"Beacon node is configured for a different network";
|
||||
"endpoint" => %self.beacon_node,
|
||||
"bn_genesis_fork" => ?beacon_node_spec.genesis_fork_version,
|
||||
"our_genesis_fork" => ?spec.genesis_fork_version,
|
||||
);
|
||||
return Err(CandidateError::Incompatible);
|
||||
} else if beacon_node_spec.altair_fork_epoch != spec.altair_fork_epoch {
|
||||
warn!(
|
||||
log,
|
||||
"Beacon node has mismatched Altair fork epoch";
|
||||
"endpoint" => %self.beacon_node,
|
||||
"endpoint_altair_fork_epoch" => ?beacon_node_spec.altair_fork_epoch,
|
||||
"hint" => UPDATE_REQUIRED_LOG_HINT,
|
||||
);
|
||||
} else if beacon_node_spec.bellatrix_fork_epoch != spec.bellatrix_fork_epoch {
|
||||
warn!(
|
||||
log,
|
||||
"Beacon node has mismatched Bellatrix fork epoch";
|
||||
"endpoint" => %self.beacon_node,
|
||||
"endpoint_bellatrix_fork_epoch" => ?beacon_node_spec.bellatrix_fork_epoch,
|
||||
"hint" => UPDATE_REQUIRED_LOG_HINT,
|
||||
);
|
||||
} else if beacon_node_spec.capella_fork_epoch != spec.capella_fork_epoch {
|
||||
warn!(
|
||||
log,
|
||||
"Beacon node has mismatched Capella fork epoch";
|
||||
"endpoint" => %self.beacon_node,
|
||||
"endpoint_capella_fork_epoch" => ?beacon_node_spec.capella_fork_epoch,
|
||||
"hint" => UPDATE_REQUIRED_LOG_HINT,
|
||||
);
|
||||
} else if beacon_node_spec.deneb_fork_epoch != spec.deneb_fork_epoch {
|
||||
warn!(
|
||||
log,
|
||||
"Beacon node has mismatched Deneb fork epoch";
|
||||
"endpoint" => %self.beacon_node,
|
||||
"endpoint_deneb_fork_epoch" => ?beacon_node_spec.deneb_fork_epoch,
|
||||
"hint" => UPDATE_REQUIRED_LOG_HINT,
|
||||
);
|
||||
} else if beacon_node_spec.electra_fork_epoch != spec.electra_fork_epoch {
|
||||
warn!(
|
||||
log,
|
||||
"Beacon node has mismatched Electra fork epoch";
|
||||
"endpoint" => %self.beacon_node,
|
||||
"endpoint_electra_fork_epoch" => ?beacon_node_spec.electra_fork_epoch,
|
||||
"hint" => UPDATE_REQUIRED_LOG_HINT,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of `CandidateBeaconNode` that can be used to perform requests with "fallback"
|
||||
/// behaviour, where the failure of one candidate results in the next candidate receiving an
|
||||
/// identical query.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BeaconNodeFallback<T, E> {
|
||||
pub candidates: Arc<RwLock<Vec<CandidateBeaconNode<E>>>>,
|
||||
distance_tiers: BeaconNodeSyncDistanceTiers,
|
||||
slot_clock: Option<T>,
|
||||
broadcast_topics: Vec<ApiTopic>,
|
||||
spec: Arc<ChainSpec>,
|
||||
log: Logger,
|
||||
}
|
||||
|
||||
impl<T: SlotClock, E: EthSpec> BeaconNodeFallback<T, E> {
|
||||
pub fn new(
|
||||
candidates: Vec<CandidateBeaconNode<E>>,
|
||||
config: Config,
|
||||
broadcast_topics: Vec<ApiTopic>,
|
||||
spec: Arc<ChainSpec>,
|
||||
log: Logger,
|
||||
) -> Self {
|
||||
let distance_tiers = config.sync_tolerances;
|
||||
Self {
|
||||
candidates: Arc::new(RwLock::new(candidates)),
|
||||
distance_tiers,
|
||||
slot_clock: None,
|
||||
broadcast_topics,
|
||||
spec,
|
||||
log,
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to update the slot clock post-instantiation.
|
||||
///
|
||||
/// This is the result of a chicken-and-egg issue where `Self` needs a slot clock for some
|
||||
/// operations, but `Self` is required to obtain the slot clock since we need the genesis time
|
||||
/// from a beacon node.
|
||||
pub fn set_slot_clock(&mut self, slot_clock: T) {
|
||||
self.slot_clock = Some(slot_clock);
|
||||
}
|
||||
|
||||
/// The count of candidates, regardless of their state.
|
||||
pub async fn num_total(&self) -> usize {
|
||||
self.candidates.read().await.len()
|
||||
}
|
||||
|
||||
/// The count of candidates that are online and compatible, but not necessarily synced.
|
||||
pub async fn num_available(&self) -> usize {
|
||||
let mut n = 0;
|
||||
for candidate in self.candidates.read().await.iter() {
|
||||
match candidate.health().await {
|
||||
Ok(_) | Err(CandidateError::Uninitialized) => n += 1,
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
// Returns all data required by the VC notifier.
|
||||
pub async fn get_notifier_info(&self) -> (Vec<CandidateInfo>, usize, usize) {
|
||||
let candidates = self.candidates.read().await;
|
||||
|
||||
let mut candidate_info = Vec::with_capacity(candidates.len());
|
||||
let mut num_available = 0;
|
||||
let mut num_synced = 0;
|
||||
|
||||
for candidate in candidates.iter() {
|
||||
let health = candidate.health().await;
|
||||
|
||||
match health {
|
||||
Ok(health) => {
|
||||
if self
|
||||
.distance_tiers
|
||||
.compute_distance_tier(health.health_tier.sync_distance)
|
||||
== SyncDistanceTier::Synced
|
||||
{
|
||||
num_synced += 1;
|
||||
}
|
||||
num_available += 1;
|
||||
}
|
||||
Err(CandidateError::Uninitialized) => num_available += 1,
|
||||
Err(_) => (),
|
||||
}
|
||||
|
||||
candidate_info.push(CandidateInfo {
|
||||
index: candidate.index,
|
||||
endpoint: candidate.beacon_node.to_string(),
|
||||
health,
|
||||
});
|
||||
}
|
||||
|
||||
(candidate_info, num_available, num_synced)
|
||||
}
|
||||
|
||||
/// Loop through ALL candidates in `self.candidates` and update their sync status.
|
||||
///
|
||||
/// It is possible for a node to return an unsynced status while continuing to serve
|
||||
/// low quality responses. To route around this it's best to poll all connected beacon nodes.
|
||||
/// A previous implementation of this function polled only the unavailable BNs.
|
||||
pub async fn update_all_candidates(&self) {
|
||||
// Clone the vec, so we release the read lock immediately.
|
||||
// `candidate.health` is behind an Arc<RwLock>, so this would still allow us to mutate the values.
|
||||
let candidates = self.candidates.read().await.clone();
|
||||
let mut futures = Vec::with_capacity(candidates.len());
|
||||
let mut nodes = Vec::with_capacity(candidates.len());
|
||||
|
||||
for candidate in candidates.iter() {
|
||||
futures.push(candidate.refresh_health(
|
||||
&self.distance_tiers,
|
||||
self.slot_clock.as_ref(),
|
||||
&self.spec,
|
||||
&self.log,
|
||||
));
|
||||
nodes.push(candidate.beacon_node.to_string());
|
||||
}
|
||||
|
||||
// Run all updates concurrently.
|
||||
let future_results = future::join_all(futures).await;
|
||||
let results = future_results.iter().zip(nodes);
|
||||
|
||||
for (result, node) in results {
|
||||
if let Err(e) = result {
|
||||
if *e != CandidateError::PreGenesis {
|
||||
warn!(
|
||||
self.log,
|
||||
"A connected beacon node errored during routine health check";
|
||||
"error" => ?e,
|
||||
"endpoint" => node,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(candidates);
|
||||
|
||||
let mut candidates = self.candidates.write().await;
|
||||
sort_nodes_by_health(&mut candidates).await;
|
||||
}
|
||||
|
||||
/// Concurrently send a request to all candidates (regardless of
|
||||
/// offline/online) status and attempt to collect a rough reading on the
|
||||
/// latency between the VC and candidate.
|
||||
pub async fn measure_latency(&self) -> Vec<LatencyMeasurement> {
|
||||
let candidates = self.candidates.read().await;
|
||||
let futures: Vec<_> = candidates
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|candidate| async move {
|
||||
let beacon_node_id = candidate.beacon_node.to_string();
|
||||
// The `node/version` endpoint is used since I imagine it would
|
||||
// require the least processing in the BN and therefore measure
|
||||
// the connection moreso than the BNs processing speed.
|
||||
//
|
||||
// I imagine all clients have the version string availble as a
|
||||
// pre-computed string.
|
||||
let response_instant = candidate
|
||||
.beacon_node
|
||||
.get_node_version()
|
||||
.await
|
||||
.ok()
|
||||
.map(|_| Instant::now());
|
||||
(beacon_node_id, response_instant)
|
||||
})
|
||||
.collect();
|
||||
drop(candidates);
|
||||
|
||||
let request_instant = Instant::now();
|
||||
|
||||
// Send the request to all BNs at the same time. This might involve some
|
||||
// queueing on the sending host, however I hope it will avoid bias
|
||||
// caused by sending requests at different times.
|
||||
future::join_all(futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(beacon_node_id, response_instant)| LatencyMeasurement {
|
||||
beacon_node_id,
|
||||
latency: response_instant
|
||||
.and_then(|response| response.checked_duration_since(request_instant)),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Run `func` against each candidate in `self`, returning immediately if a result is found.
|
||||
/// Otherwise, return all the errors encountered along the way.
|
||||
pub async fn first_success<F, O, Err, R>(&self, func: F) -> Result<O, Errors<Err>>
|
||||
where
|
||||
F: Fn(BeaconNodeHttpClient) -> R,
|
||||
R: Future<Output = Result<O, Err>>,
|
||||
Err: Debug,
|
||||
{
|
||||
let mut errors = vec![];
|
||||
|
||||
// First pass: try `func` on all candidates. Candidate order has already been set in
|
||||
// `update_all_candidates`. This ensures the most suitable node is always tried first.
|
||||
let candidates = self.candidates.read().await;
|
||||
let mut futures = vec![];
|
||||
|
||||
// Run `func` using a `candidate`, returning the value or capturing errors.
|
||||
for candidate in candidates.iter() {
|
||||
futures.push(Self::run_on_candidate(
|
||||
candidate.beacon_node.clone(),
|
||||
&func,
|
||||
&self.log,
|
||||
));
|
||||
}
|
||||
drop(candidates);
|
||||
|
||||
for future in futures {
|
||||
match future.await {
|
||||
Ok(val) => return Ok(val),
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass. No candidates returned successfully. Try again with the same order.
|
||||
// This will duplicate errors.
|
||||
let candidates = self.candidates.read().await;
|
||||
let mut futures = vec![];
|
||||
|
||||
// Run `func` using a `candidate`, returning the value or capturing errors.
|
||||
for candidate in candidates.iter() {
|
||||
futures.push(Self::run_on_candidate(
|
||||
candidate.beacon_node.clone(),
|
||||
&func,
|
||||
&self.log,
|
||||
));
|
||||
}
|
||||
drop(candidates);
|
||||
|
||||
for future in futures {
|
||||
match future.await {
|
||||
Ok(val) => return Ok(val),
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
|
||||
// No candidates returned successfully.
|
||||
Err(Errors(errors))
|
||||
}
|
||||
|
||||
/// Run the future `func` on `candidate` while reporting metrics.
|
||||
async fn run_on_candidate<F, R, Err, O>(
|
||||
candidate: BeaconNodeHttpClient,
|
||||
func: F,
|
||||
log: &Logger,
|
||||
) -> Result<O, (String, Error<Err>)>
|
||||
where
|
||||
F: Fn(BeaconNodeHttpClient) -> R,
|
||||
R: Future<Output = Result<O, Err>>,
|
||||
Err: Debug,
|
||||
{
|
||||
inc_counter_vec(&ENDPOINT_REQUESTS, &[candidate.as_ref()]);
|
||||
|
||||
// There exists a race condition where `func` may be called when the candidate is
|
||||
// actually not ready. We deem this an acceptable inefficiency.
|
||||
match func(candidate.clone()).await {
|
||||
Ok(val) => Ok(val),
|
||||
Err(e) => {
|
||||
debug!(
|
||||
log,
|
||||
"Request to beacon node failed";
|
||||
"node" => %candidate,
|
||||
"error" => ?e,
|
||||
);
|
||||
inc_counter_vec(&ENDPOINT_ERRORS, &[candidate.as_ref()]);
|
||||
Err((candidate.to_string(), Error::RequestFailed(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `func` against all candidates in `self`, collecting the result of `func` against each
|
||||
/// candidate.
|
||||
///
|
||||
/// Note: This function returns `Ok(())` if `func` returned successfully on all beacon nodes.
|
||||
/// It returns a list of errors along with the beacon node id that failed for `func`.
|
||||
/// Since this ignores the actual result of `func`, this function should only be used for beacon
|
||||
/// node calls whose results we do not care about, only that they completed successfully.
|
||||
pub async fn broadcast<F, O, Err, R>(&self, func: F) -> Result<(), Errors<Err>>
|
||||
where
|
||||
F: Fn(BeaconNodeHttpClient) -> R,
|
||||
R: Future<Output = Result<O, Err>>,
|
||||
Err: Debug,
|
||||
{
|
||||
// Run `func` on all candidates.
|
||||
let candidates = self.candidates.read().await;
|
||||
let mut futures = vec![];
|
||||
|
||||
// Run `func` using a `candidate`, returning the value or capturing errors.
|
||||
for candidate in candidates.iter() {
|
||||
futures.push(Self::run_on_candidate(
|
||||
candidate.beacon_node.clone(),
|
||||
&func,
|
||||
&self.log,
|
||||
));
|
||||
}
|
||||
drop(candidates);
|
||||
|
||||
let results = future::join_all(futures).await;
|
||||
|
||||
let errors: Vec<_> = results.into_iter().filter_map(|res| res.err()).collect();
|
||||
|
||||
if !errors.is_empty() {
|
||||
Err(Errors(errors))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Call `func` on first beacon node that returns success or on all beacon nodes
|
||||
/// depending on the `topic` and configuration.
|
||||
pub async fn request<F, Err, R>(&self, topic: ApiTopic, func: F) -> Result<(), Errors<Err>>
|
||||
where
|
||||
F: Fn(BeaconNodeHttpClient) -> R,
|
||||
R: Future<Output = Result<(), Err>>,
|
||||
Err: Debug,
|
||||
{
|
||||
if self.broadcast_topics.contains(&topic) {
|
||||
self.broadcast(func).await
|
||||
} else {
|
||||
self.first_success(func).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper functions to allow sorting candidate nodes by health.
|
||||
async fn sort_nodes_by_health<E: EthSpec>(nodes: &mut Vec<CandidateBeaconNode<E>>) {
|
||||
// Fetch all health values.
|
||||
let health_results: Vec<Result<BeaconNodeHealth, CandidateError>> =
|
||||
future::join_all(nodes.iter().map(|node| node.health())).await;
|
||||
|
||||
// Pair health results with their indices.
|
||||
let mut indices_with_health: Vec<(usize, Result<BeaconNodeHealth, CandidateError>)> =
|
||||
health_results.into_iter().enumerate().collect();
|
||||
|
||||
// Sort indices based on their health.
|
||||
indices_with_health.sort_by(|a, b| match (&a.1, &b.1) {
|
||||
(Ok(health_a), Ok(health_b)) => health_a.cmp(health_b),
|
||||
(Err(_), Ok(_)) => Ordering::Greater,
|
||||
(Ok(_), Err(_)) => Ordering::Less,
|
||||
(Err(_), Err(_)) => Ordering::Equal,
|
||||
});
|
||||
|
||||
// Reorder candidates based on the sorted indices.
|
||||
let sorted_nodes: Vec<CandidateBeaconNode<E>> = indices_with_health
|
||||
.into_iter()
|
||||
.map(|(index, _)| nodes[index].clone())
|
||||
.collect();
|
||||
*nodes = sorted_nodes;
|
||||
}
|
||||
|
||||
/// Serves as a cue for `BeaconNodeFallback` to tell which requests need to be broadcasted.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, EnumString, EnumVariantNames)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum ApiTopic {
|
||||
Attestations,
|
||||
Blocks,
|
||||
Subscriptions,
|
||||
SyncCommittee,
|
||||
}
|
||||
|
||||
impl ApiTopic {
|
||||
pub fn all() -> Vec<ApiTopic> {
|
||||
use ApiTopic::*;
|
||||
vec![Attestations, Blocks, Subscriptions, SyncCommittee]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::beacon_node_health::BeaconNodeHealthTier;
|
||||
use crate::SensitiveUrl;
|
||||
use eth2::Timeouts;
|
||||
use std::str::FromStr;
|
||||
use strum::VariantNames;
|
||||
use types::{MainnetEthSpec, Slot};
|
||||
|
||||
type E = MainnetEthSpec;
|
||||
|
||||
#[test]
|
||||
fn api_topic_all() {
|
||||
let all = ApiTopic::all();
|
||||
assert_eq!(all.len(), ApiTopic::VARIANTS.len());
|
||||
assert!(ApiTopic::VARIANTS
|
||||
.iter()
|
||||
.map(|topic| ApiTopic::from_str(topic).unwrap())
|
||||
.eq(all.into_iter()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_candidate_order() {
|
||||
// These fields is irrelvant for sorting. They are set to arbitrary values.
|
||||
let head = Slot::new(99);
|
||||
let optimistic_status = IsOptimistic::No;
|
||||
let execution_status = ExecutionEngineHealth::Healthy;
|
||||
|
||||
fn new_candidate(index: usize) -> CandidateBeaconNode<E> {
|
||||
let beacon_node = BeaconNodeHttpClient::new(
|
||||
SensitiveUrl::parse(&format!("http://example_{index}.com")).unwrap(),
|
||||
Timeouts::set_all(Duration::from_secs(index as u64)),
|
||||
);
|
||||
CandidateBeaconNode::new(beacon_node, index)
|
||||
}
|
||||
|
||||
let candidate_1 = new_candidate(1);
|
||||
let expected_candidate_1 = new_candidate(1);
|
||||
let candidate_2 = new_candidate(2);
|
||||
let expected_candidate_2 = new_candidate(2);
|
||||
let candidate_3 = new_candidate(3);
|
||||
let expected_candidate_3 = new_candidate(3);
|
||||
let candidate_4 = new_candidate(4);
|
||||
let expected_candidate_4 = new_candidate(4);
|
||||
let candidate_5 = new_candidate(5);
|
||||
let expected_candidate_5 = new_candidate(5);
|
||||
let candidate_6 = new_candidate(6);
|
||||
let expected_candidate_6 = new_candidate(6);
|
||||
|
||||
let synced = SyncDistanceTier::Synced;
|
||||
let small = SyncDistanceTier::Small;
|
||||
|
||||
// Despite `health_1` having a larger sync distance, it is inside the `synced` range which
|
||||
// does not tie-break on sync distance and so will tie-break on `user_index` instead.
|
||||
let health_1 = BeaconNodeHealth {
|
||||
user_index: 1,
|
||||
head,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
health_tier: BeaconNodeHealthTier::new(1, Slot::new(2), synced),
|
||||
};
|
||||
let health_2 = BeaconNodeHealth {
|
||||
user_index: 2,
|
||||
head,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
health_tier: BeaconNodeHealthTier::new(2, Slot::new(1), synced),
|
||||
};
|
||||
|
||||
// `health_3` and `health_4` have the same health tier and sync distance so should
|
||||
// tie-break on `user_index`.
|
||||
let health_3 = BeaconNodeHealth {
|
||||
user_index: 3,
|
||||
head,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
health_tier: BeaconNodeHealthTier::new(3, Slot::new(9), small),
|
||||
};
|
||||
let health_4 = BeaconNodeHealth {
|
||||
user_index: 4,
|
||||
head,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
health_tier: BeaconNodeHealthTier::new(3, Slot::new(9), small),
|
||||
};
|
||||
|
||||
// `health_5` has a smaller sync distance and is outside the `synced` range so should be
|
||||
// sorted first. Note the values of `user_index`.
|
||||
let health_5 = BeaconNodeHealth {
|
||||
user_index: 6,
|
||||
head,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
health_tier: BeaconNodeHealthTier::new(4, Slot::new(9), small),
|
||||
};
|
||||
let health_6 = BeaconNodeHealth {
|
||||
user_index: 5,
|
||||
head,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
health_tier: BeaconNodeHealthTier::new(4, Slot::new(10), small),
|
||||
};
|
||||
|
||||
*candidate_1.health.write().await = Ok(health_1);
|
||||
*candidate_2.health.write().await = Ok(health_2);
|
||||
*candidate_3.health.write().await = Ok(health_3);
|
||||
*candidate_4.health.write().await = Ok(health_4);
|
||||
*candidate_5.health.write().await = Ok(health_5);
|
||||
*candidate_6.health.write().await = Ok(health_6);
|
||||
|
||||
let mut candidates = vec![
|
||||
candidate_3,
|
||||
candidate_6,
|
||||
candidate_5,
|
||||
candidate_1,
|
||||
candidate_4,
|
||||
candidate_2,
|
||||
];
|
||||
let expected_candidates = vec![
|
||||
expected_candidate_1,
|
||||
expected_candidate_2,
|
||||
expected_candidate_3,
|
||||
expected_candidate_4,
|
||||
expected_candidate_5,
|
||||
expected_candidate_6,
|
||||
];
|
||||
|
||||
sort_nodes_by_health(&mut candidates).await;
|
||||
|
||||
assert_eq!(candidates, expected_candidates);
|
||||
}
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use types::Slot;
|
||||
|
||||
/// Sync distances between 0 and DEFAULT_SYNC_TOLERANCE are considered `synced`.
|
||||
/// Sync distance tiers are determined by the different modifiers.
|
||||
///
|
||||
/// The default range is the following:
|
||||
/// Synced: 0..=8
|
||||
/// Small: 9..=16
|
||||
/// Medium: 17..=64
|
||||
/// Large: 65..
|
||||
const DEFAULT_SYNC_TOLERANCE: Slot = Slot::new(8);
|
||||
const DEFAULT_SMALL_SYNC_DISTANCE_MODIFIER: Slot = Slot::new(8);
|
||||
const DEFAULT_MEDIUM_SYNC_DISTANCE_MODIFIER: Slot = Slot::new(48);
|
||||
|
||||
type HealthTier = u8;
|
||||
type SyncDistance = Slot;
|
||||
|
||||
/// Helpful enum which is used when pattern matching to determine health tier.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum SyncDistanceTier {
|
||||
Synced,
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
/// Contains the different sync distance tiers which are determined at runtime by the
|
||||
/// `beacon-nodes-sync-tolerances` flag.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct BeaconNodeSyncDistanceTiers {
|
||||
pub synced: SyncDistance,
|
||||
pub small: SyncDistance,
|
||||
pub medium: SyncDistance,
|
||||
}
|
||||
|
||||
impl Default for BeaconNodeSyncDistanceTiers {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
synced: DEFAULT_SYNC_TOLERANCE,
|
||||
small: DEFAULT_SYNC_TOLERANCE + DEFAULT_SMALL_SYNC_DISTANCE_MODIFIER,
|
||||
medium: DEFAULT_SYNC_TOLERANCE
|
||||
+ DEFAULT_SMALL_SYNC_DISTANCE_MODIFIER
|
||||
+ DEFAULT_MEDIUM_SYNC_DISTANCE_MODIFIER,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for BeaconNodeSyncDistanceTiers {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, String> {
|
||||
let values: (u64, u64, u64) = s
|
||||
.split(',')
|
||||
.map(|s| {
|
||||
s.parse()
|
||||
.map_err(|e| format!("Invalid sync distance modifier: {e:?}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.collect_tuple()
|
||||
.ok_or("Invalid number of sync distance modifiers".to_string())?;
|
||||
|
||||
Ok(BeaconNodeSyncDistanceTiers {
|
||||
synced: Slot::new(values.0),
|
||||
small: Slot::new(values.0 + values.1),
|
||||
medium: Slot::new(values.0 + values.1 + values.2),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BeaconNodeSyncDistanceTiers {
|
||||
/// Takes a given sync distance and determines its tier based on the `sync_tolerance` defined by
|
||||
/// the CLI.
|
||||
pub fn compute_distance_tier(&self, distance: SyncDistance) -> SyncDistanceTier {
|
||||
if distance <= self.synced {
|
||||
SyncDistanceTier::Synced
|
||||
} else if distance <= self.small {
|
||||
SyncDistanceTier::Small
|
||||
} else if distance <= self.medium {
|
||||
SyncDistanceTier::Medium
|
||||
} else {
|
||||
SyncDistanceTier::Large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execution Node health metrics.
|
||||
///
|
||||
/// Currently only considers `el_offline`.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum ExecutionEngineHealth {
|
||||
Healthy,
|
||||
Unhealthy,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum IsOptimistic {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct BeaconNodeHealthTier {
|
||||
pub tier: HealthTier,
|
||||
pub sync_distance: SyncDistance,
|
||||
pub distance_tier: SyncDistanceTier,
|
||||
}
|
||||
|
||||
impl Display for BeaconNodeHealthTier {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Tier{}({})", self.tier, self.sync_distance)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for BeaconNodeHealthTier {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
let ordering = self.tier.cmp(&other.tier);
|
||||
if ordering == Ordering::Equal {
|
||||
if self.distance_tier == SyncDistanceTier::Synced {
|
||||
// Don't tie-break on sync distance in these cases.
|
||||
// This ensures validator clients don't artificially prefer one node.
|
||||
ordering
|
||||
} else {
|
||||
self.sync_distance.cmp(&other.sync_distance)
|
||||
}
|
||||
} else {
|
||||
ordering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for BeaconNodeHealthTier {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl BeaconNodeHealthTier {
|
||||
pub fn new(
|
||||
tier: HealthTier,
|
||||
sync_distance: SyncDistance,
|
||||
distance_tier: SyncDistanceTier,
|
||||
) -> Self {
|
||||
Self {
|
||||
tier,
|
||||
sync_distance,
|
||||
distance_tier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Beacon Node Health metrics.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct BeaconNodeHealth {
|
||||
// The index of the Beacon Node. This should correspond with its position in the
|
||||
// `--beacon-nodes` list. Note that the `user_index` field is used to tie-break nodes with the
|
||||
// same health so that nodes with a lower index are preferred.
|
||||
pub user_index: usize,
|
||||
// The slot number of the head.
|
||||
pub head: Slot,
|
||||
// Whether the node is optimistically synced.
|
||||
pub optimistic_status: IsOptimistic,
|
||||
// The status of the nodes connected Execution Engine.
|
||||
pub execution_status: ExecutionEngineHealth,
|
||||
// The overall health tier of the Beacon Node. Used to rank the nodes for the purposes of
|
||||
// fallbacks.
|
||||
pub health_tier: BeaconNodeHealthTier,
|
||||
}
|
||||
|
||||
impl Ord for BeaconNodeHealth {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
let ordering = self.health_tier.cmp(&other.health_tier);
|
||||
if ordering == Ordering::Equal {
|
||||
// Tie-break node health by `user_index`.
|
||||
self.user_index.cmp(&other.user_index)
|
||||
} else {
|
||||
ordering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for BeaconNodeHealth {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl BeaconNodeHealth {
|
||||
pub fn from_status(
|
||||
user_index: usize,
|
||||
sync_distance: Slot,
|
||||
head: Slot,
|
||||
optimistic_status: IsOptimistic,
|
||||
execution_status: ExecutionEngineHealth,
|
||||
distance_tiers: &BeaconNodeSyncDistanceTiers,
|
||||
) -> Self {
|
||||
let health_tier = BeaconNodeHealth::compute_health_tier(
|
||||
sync_distance,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
distance_tiers,
|
||||
);
|
||||
|
||||
Self {
|
||||
user_index,
|
||||
head,
|
||||
optimistic_status,
|
||||
execution_status,
|
||||
health_tier,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> usize {
|
||||
self.user_index
|
||||
}
|
||||
|
||||
pub fn get_health_tier(&self) -> BeaconNodeHealthTier {
|
||||
self.health_tier
|
||||
}
|
||||
|
||||
fn compute_health_tier(
|
||||
sync_distance: SyncDistance,
|
||||
optimistic_status: IsOptimistic,
|
||||
execution_status: ExecutionEngineHealth,
|
||||
sync_distance_tiers: &BeaconNodeSyncDistanceTiers,
|
||||
) -> BeaconNodeHealthTier {
|
||||
let sync_distance_tier = sync_distance_tiers.compute_distance_tier(sync_distance);
|
||||
let health = (sync_distance_tier, optimistic_status, execution_status);
|
||||
|
||||
match health {
|
||||
(SyncDistanceTier::Synced, IsOptimistic::No, ExecutionEngineHealth::Healthy) => {
|
||||
BeaconNodeHealthTier::new(1, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Small, IsOptimistic::No, ExecutionEngineHealth::Healthy) => {
|
||||
BeaconNodeHealthTier::new(2, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Synced, IsOptimistic::No, ExecutionEngineHealth::Unhealthy) => {
|
||||
BeaconNodeHealthTier::new(3, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Medium, IsOptimistic::No, ExecutionEngineHealth::Healthy) => {
|
||||
BeaconNodeHealthTier::new(4, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Synced, IsOptimistic::Yes, ExecutionEngineHealth::Healthy) => {
|
||||
BeaconNodeHealthTier::new(5, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Synced, IsOptimistic::Yes, ExecutionEngineHealth::Unhealthy) => {
|
||||
BeaconNodeHealthTier::new(6, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Small, IsOptimistic::No, ExecutionEngineHealth::Unhealthy) => {
|
||||
BeaconNodeHealthTier::new(7, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Small, IsOptimistic::Yes, ExecutionEngineHealth::Healthy) => {
|
||||
BeaconNodeHealthTier::new(8, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Small, IsOptimistic::Yes, ExecutionEngineHealth::Unhealthy) => {
|
||||
BeaconNodeHealthTier::new(9, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Large, IsOptimistic::No, ExecutionEngineHealth::Healthy) => {
|
||||
BeaconNodeHealthTier::new(10, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Medium, IsOptimistic::No, ExecutionEngineHealth::Unhealthy) => {
|
||||
BeaconNodeHealthTier::new(11, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Medium, IsOptimistic::Yes, ExecutionEngineHealth::Healthy) => {
|
||||
BeaconNodeHealthTier::new(12, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Medium, IsOptimistic::Yes, ExecutionEngineHealth::Unhealthy) => {
|
||||
BeaconNodeHealthTier::new(13, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Large, IsOptimistic::No, ExecutionEngineHealth::Unhealthy) => {
|
||||
BeaconNodeHealthTier::new(14, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Large, IsOptimistic::Yes, ExecutionEngineHealth::Healthy) => {
|
||||
BeaconNodeHealthTier::new(15, sync_distance, sync_distance_tier)
|
||||
}
|
||||
(SyncDistanceTier::Large, IsOptimistic::Yes, ExecutionEngineHealth::Unhealthy) => {
|
||||
BeaconNodeHealthTier::new(16, sync_distance, sync_distance_tier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ExecutionEngineHealth::{Healthy, Unhealthy};
|
||||
use super::{
|
||||
BeaconNodeHealth, BeaconNodeHealthTier, BeaconNodeSyncDistanceTiers, IsOptimistic,
|
||||
SyncDistanceTier,
|
||||
};
|
||||
use crate::beacon_node_fallback::Config;
|
||||
use std::str::FromStr;
|
||||
use types::Slot;
|
||||
|
||||
#[test]
|
||||
fn all_possible_health_tiers() {
|
||||
let config = Config::default();
|
||||
let beacon_node_sync_distance_tiers = config.sync_tolerances;
|
||||
|
||||
let mut health_vec = vec![];
|
||||
|
||||
for head_slot in 0..=64 {
|
||||
for optimistic_status in &[IsOptimistic::No, IsOptimistic::Yes] {
|
||||
for ee_health in &[Healthy, Unhealthy] {
|
||||
let health = BeaconNodeHealth::from_status(
|
||||
0,
|
||||
Slot::new(0),
|
||||
Slot::new(head_slot),
|
||||
*optimistic_status,
|
||||
*ee_health,
|
||||
&beacon_node_sync_distance_tiers,
|
||||
);
|
||||
health_vec.push(health);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for health in health_vec {
|
||||
let health_tier = health.get_health_tier();
|
||||
let tier = health_tier.tier;
|
||||
let distance = health_tier.sync_distance;
|
||||
|
||||
let distance_tier = beacon_node_sync_distance_tiers.compute_distance_tier(distance);
|
||||
|
||||
// Check sync distance.
|
||||
if [1, 3, 5, 6].contains(&tier) {
|
||||
assert!(distance_tier == SyncDistanceTier::Synced)
|
||||
} else if [2, 7, 8, 9].contains(&tier) {
|
||||
assert!(distance_tier == SyncDistanceTier::Small);
|
||||
} else if [4, 11, 12, 13].contains(&tier) {
|
||||
assert!(distance_tier == SyncDistanceTier::Medium);
|
||||
} else {
|
||||
assert!(distance_tier == SyncDistanceTier::Large);
|
||||
}
|
||||
|
||||
// Check optimistic status.
|
||||
if [1, 2, 3, 4, 7, 10, 11, 14].contains(&tier) {
|
||||
assert_eq!(health.optimistic_status, IsOptimistic::No);
|
||||
} else {
|
||||
assert_eq!(health.optimistic_status, IsOptimistic::Yes);
|
||||
}
|
||||
|
||||
// Check execution health.
|
||||
if [3, 6, 7, 9, 11, 13, 14, 16].contains(&tier) {
|
||||
assert_eq!(health.execution_status, Unhealthy);
|
||||
} else {
|
||||
assert_eq!(health.execution_status, Healthy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_distance_tier(
|
||||
distance: u64,
|
||||
distance_tiers: &BeaconNodeSyncDistanceTiers,
|
||||
) -> BeaconNodeHealthTier {
|
||||
BeaconNodeHealth::compute_health_tier(
|
||||
Slot::new(distance),
|
||||
IsOptimistic::No,
|
||||
Healthy,
|
||||
distance_tiers,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_tolerance_default() {
|
||||
let distance_tiers = BeaconNodeSyncDistanceTiers::default();
|
||||
|
||||
let synced_low = new_distance_tier(0, &distance_tiers);
|
||||
let synced_high = new_distance_tier(8, &distance_tiers);
|
||||
|
||||
let small_low = new_distance_tier(9, &distance_tiers);
|
||||
let small_high = new_distance_tier(16, &distance_tiers);
|
||||
|
||||
let medium_low = new_distance_tier(17, &distance_tiers);
|
||||
let medium_high = new_distance_tier(64, &distance_tiers);
|
||||
let large = new_distance_tier(65, &distance_tiers);
|
||||
|
||||
assert_eq!(synced_low.tier, 1);
|
||||
assert_eq!(synced_high.tier, 1);
|
||||
assert_eq!(small_low.tier, 2);
|
||||
assert_eq!(small_high.tier, 2);
|
||||
assert_eq!(medium_low.tier, 4);
|
||||
assert_eq!(medium_high.tier, 4);
|
||||
assert_eq!(large.tier, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_tolerance_from_str() {
|
||||
// String should set the tiers as:
|
||||
// synced: 0..=4
|
||||
// small: 5..=8
|
||||
// medium 9..=12
|
||||
// large: 13..
|
||||
|
||||
let distance_tiers = BeaconNodeSyncDistanceTiers::from_str("4,4,4").unwrap();
|
||||
|
||||
let synced_low = new_distance_tier(0, &distance_tiers);
|
||||
let synced_high = new_distance_tier(4, &distance_tiers);
|
||||
|
||||
let small_low = new_distance_tier(5, &distance_tiers);
|
||||
let small_high = new_distance_tier(8, &distance_tiers);
|
||||
|
||||
let medium_low = new_distance_tier(9, &distance_tiers);
|
||||
let medium_high = new_distance_tier(12, &distance_tiers);
|
||||
|
||||
let large = new_distance_tier(13, &distance_tiers);
|
||||
|
||||
assert_eq!(synced_low.tier, 1);
|
||||
assert_eq!(synced_high.tier, 1);
|
||||
assert_eq!(small_low.tier, 2);
|
||||
assert_eq!(small_high.tier, 2);
|
||||
assert_eq!(medium_low.tier, 4);
|
||||
assert_eq!(medium_high.tier, 4);
|
||||
assert_eq!(large.tier, 10);
|
||||
}
|
||||
}
|
||||
@@ -1,691 +0,0 @@
|
||||
use crate::beacon_node_fallback::{Error as FallbackError, Errors};
|
||||
use crate::{
|
||||
beacon_node_fallback::{ApiTopic, BeaconNodeFallback},
|
||||
determine_graffiti,
|
||||
graffiti_file::GraffitiFile,
|
||||
};
|
||||
use crate::{
|
||||
http_metrics::metrics,
|
||||
validator_store::{Error as ValidatorStoreError, ValidatorStore},
|
||||
};
|
||||
use bls::SignatureBytes;
|
||||
use environment::RuntimeContext;
|
||||
use eth2::types::{FullBlockContents, PublishBlockRequest};
|
||||
use eth2::{BeaconNodeHttpClient, StatusCode};
|
||||
use slog::{crit, debug, error, info, trace, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use types::{
|
||||
BlindedBeaconBlock, BlockType, EthSpec, Graffiti, PublicKeyBytes, SignedBlindedBeaconBlock,
|
||||
Slot,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BlockError {
|
||||
/// A recoverable error that can be retried, as the validator has not signed anything.
|
||||
Recoverable(String),
|
||||
/// An irrecoverable error has occurred during block proposal and should not be retried, as a
|
||||
/// block may have already been signed.
|
||||
Irrecoverable(String),
|
||||
}
|
||||
|
||||
impl From<Errors<BlockError>> for BlockError {
|
||||
fn from(e: Errors<BlockError>) -> Self {
|
||||
if e.0.iter().any(|(_, error)| {
|
||||
matches!(
|
||||
error,
|
||||
FallbackError::RequestFailed(BlockError::Irrecoverable(_))
|
||||
)
|
||||
}) {
|
||||
BlockError::Irrecoverable(e.to_string())
|
||||
} else {
|
||||
BlockError::Recoverable(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `BlockService`.
|
||||
pub struct BlockServiceBuilder<T, E: EthSpec> {
|
||||
validator_store: Option<Arc<ValidatorStore<T, E>>>,
|
||||
slot_clock: Option<Arc<T>>,
|
||||
beacon_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
|
||||
proposer_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
|
||||
context: Option<RuntimeContext<E>>,
|
||||
graffiti: Option<Graffiti>,
|
||||
graffiti_file: Option<GraffitiFile>,
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
validator_store: None,
|
||||
slot_clock: None,
|
||||
beacon_nodes: None,
|
||||
proposer_nodes: None,
|
||||
context: None,
|
||||
graffiti: None,
|
||||
graffiti_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validator_store(mut self, store: Arc<ValidatorStore<T, E>>) -> Self {
|
||||
self.validator_store = Some(store);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn slot_clock(mut self, slot_clock: T) -> Self {
|
||||
self.slot_clock = Some(Arc::new(slot_clock));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn beacon_nodes(mut self, beacon_nodes: Arc<BeaconNodeFallback<T, E>>) -> Self {
|
||||
self.beacon_nodes = Some(beacon_nodes);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn proposer_nodes(mut self, proposer_nodes: Arc<BeaconNodeFallback<T, E>>) -> Self {
|
||||
self.proposer_nodes = Some(proposer_nodes);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn runtime_context(mut self, context: RuntimeContext<E>) -> Self {
|
||||
self.context = Some(context);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn graffiti(mut self, graffiti: Option<Graffiti>) -> Self {
|
||||
self.graffiti = graffiti;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn graffiti_file(mut self, graffiti_file: Option<GraffitiFile>) -> Self {
|
||||
self.graffiti_file = graffiti_file;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<BlockService<T, E>, String> {
|
||||
Ok(BlockService {
|
||||
inner: Arc::new(Inner {
|
||||
validator_store: self
|
||||
.validator_store
|
||||
.ok_or("Cannot build BlockService without validator_store")?,
|
||||
slot_clock: self
|
||||
.slot_clock
|
||||
.ok_or("Cannot build BlockService without slot_clock")?,
|
||||
beacon_nodes: self
|
||||
.beacon_nodes
|
||||
.ok_or("Cannot build BlockService without beacon_node")?,
|
||||
context: self
|
||||
.context
|
||||
.ok_or("Cannot build BlockService without runtime_context")?,
|
||||
proposer_nodes: self.proposer_nodes,
|
||||
graffiti: self.graffiti,
|
||||
graffiti_file: self.graffiti_file,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Combines a set of non-block-proposing `beacon_nodes` and only-block-proposing
|
||||
// `proposer_nodes`.
|
||||
pub struct ProposerFallback<T, E: EthSpec> {
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
|
||||
proposer_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
|
||||
}
|
||||
|
||||
impl<T: SlotClock, E: EthSpec> ProposerFallback<T, E> {
|
||||
// Try `func` on `self.proposer_nodes` first. If that doesn't work, try `self.beacon_nodes`.
|
||||
pub async fn request_proposers_first<F, Err, R>(&self, func: F) -> Result<(), Errors<Err>>
|
||||
where
|
||||
F: Fn(BeaconNodeHttpClient) -> R + Clone,
|
||||
R: Future<Output = Result<(), Err>>,
|
||||
Err: Debug,
|
||||
{
|
||||
// If there are proposer nodes, try calling `func` on them and return early if they are successful.
|
||||
if let Some(proposer_nodes) = &self.proposer_nodes {
|
||||
if proposer_nodes
|
||||
.request(ApiTopic::Blocks, func.clone())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// If the proposer nodes failed, try on the non-proposer nodes.
|
||||
self.beacon_nodes.request(ApiTopic::Blocks, func).await
|
||||
}
|
||||
|
||||
// Try `func` on `self.beacon_nodes` first. If that doesn't work, try `self.proposer_nodes`.
|
||||
pub async fn request_proposers_last<F, O, Err, R>(&self, func: F) -> Result<O, Errors<Err>>
|
||||
where
|
||||
F: Fn(BeaconNodeHttpClient) -> R + Clone,
|
||||
R: Future<Output = Result<O, Err>>,
|
||||
Err: Debug,
|
||||
{
|
||||
// Try running `func` on the non-proposer beacon nodes.
|
||||
let beacon_nodes_result = self.beacon_nodes.first_success(func.clone()).await;
|
||||
|
||||
match (beacon_nodes_result, &self.proposer_nodes) {
|
||||
// The non-proposer node call succeed, return the result.
|
||||
(Ok(success), _) => Ok(success),
|
||||
// The non-proposer node call failed, but we don't have any proposer nodes. Return an error.
|
||||
(Err(e), None) => Err(e),
|
||||
// The non-proposer node call failed, try the same call on the proposer nodes.
|
||||
(Err(_), Some(proposer_nodes)) => proposer_nodes.first_success(func).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to minimise `Arc` usage.
|
||||
pub struct Inner<T, E: EthSpec> {
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
slot_clock: Arc<T>,
|
||||
pub(crate) beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
|
||||
pub(crate) proposer_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
|
||||
context: RuntimeContext<E>,
|
||||
graffiti: Option<Graffiti>,
|
||||
graffiti_file: Option<GraffitiFile>,
|
||||
}
|
||||
|
||||
/// Attempts to produce attestations for any block producer(s) at the start of the epoch.
|
||||
pub struct BlockService<T, E: EthSpec> {
|
||||
inner: Arc<Inner<T, E>>,
|
||||
}
|
||||
|
||||
impl<T, E: EthSpec> Clone for BlockService<T, E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E: EthSpec> Deref for BlockService<T, E> {
|
||||
type Target = Inner<T, E>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner.deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification from the duties service that we should try to produce a block.
|
||||
pub struct BlockServiceNotification {
|
||||
pub slot: Slot,
|
||||
pub block_proposers: Vec<PublicKeyBytes>,
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
|
||||
pub fn start_update_service(
|
||||
self,
|
||||
mut notification_rx: mpsc::Receiver<BlockServiceNotification>,
|
||||
) -> Result<(), String> {
|
||||
let log = self.context.log().clone();
|
||||
|
||||
info!(log, "Block production service started");
|
||||
|
||||
let executor = self.inner.context.executor.clone();
|
||||
|
||||
executor.spawn(
|
||||
async move {
|
||||
while let Some(notif) = notification_rx.recv().await {
|
||||
self.do_update(notif).await.ok();
|
||||
}
|
||||
debug!(log, "Block service shutting down");
|
||||
},
|
||||
"block_service",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to produce a block for any block producers in the `ValidatorStore`.
|
||||
async fn do_update(&self, notification: BlockServiceNotification) -> Result<(), ()> {
|
||||
let log = self.context.log();
|
||||
let _timer =
|
||||
metrics::start_timer_vec(&metrics::BLOCK_SERVICE_TIMES, &[metrics::FULL_UPDATE]);
|
||||
|
||||
let slot = self.slot_clock.now().ok_or_else(move || {
|
||||
crit!(log, "Duties manager failed to read slot clock");
|
||||
})?;
|
||||
|
||||
if notification.slot != slot {
|
||||
warn!(
|
||||
log,
|
||||
"Skipping block production for expired slot";
|
||||
"current_slot" => slot.as_u64(),
|
||||
"notification_slot" => notification.slot.as_u64(),
|
||||
"info" => "Your machine could be overloaded"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if slot == self.context.eth2_config.spec.genesis_slot {
|
||||
debug!(
|
||||
log,
|
||||
"Not producing block at genesis slot";
|
||||
"proposers" => format!("{:?}", notification.block_proposers),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
trace!(
|
||||
log,
|
||||
"Block service update started";
|
||||
"slot" => slot.as_u64()
|
||||
);
|
||||
|
||||
let proposers = notification.block_proposers;
|
||||
|
||||
if proposers.is_empty() {
|
||||
trace!(
|
||||
log,
|
||||
"No local block proposers for this slot";
|
||||
"slot" => slot.as_u64()
|
||||
)
|
||||
} else if proposers.len() > 1 {
|
||||
error!(
|
||||
log,
|
||||
"Multiple block proposers for this slot";
|
||||
"action" => "producing blocks for all proposers",
|
||||
"num_proposers" => proposers.len(),
|
||||
"slot" => slot.as_u64(),
|
||||
)
|
||||
}
|
||||
|
||||
for validator_pubkey in proposers {
|
||||
let builder_boost_factor = self.get_builder_boost_factor(&validator_pubkey);
|
||||
let service = self.clone();
|
||||
let log = log.clone();
|
||||
self.inner.context.executor.spawn(
|
||||
async move {
|
||||
let result = service
|
||||
.publish_block(slot, validator_pubkey, builder_boost_factor)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(BlockError::Recoverable(e)) | Err(BlockError::Irrecoverable(e)) => {
|
||||
error!(
|
||||
log,
|
||||
"Error whilst producing block";
|
||||
"error" => ?e,
|
||||
"block_slot" => ?slot,
|
||||
"info" => "block v3 proposal failed, this error may or may not result in a missed block"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
"block service",
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn sign_and_publish_block(
|
||||
&self,
|
||||
proposer_fallback: ProposerFallback<T, E>,
|
||||
slot: Slot,
|
||||
graffiti: Option<Graffiti>,
|
||||
validator_pubkey: &PublicKeyBytes,
|
||||
unsigned_block: UnsignedBlock<E>,
|
||||
) -> Result<(), BlockError> {
|
||||
let log = self.context.log();
|
||||
let signing_timer = metrics::start_timer(&metrics::BLOCK_SIGNING_TIMES);
|
||||
|
||||
let res = match unsigned_block {
|
||||
UnsignedBlock::Full(block_contents) => {
|
||||
let (block, maybe_blobs) = block_contents.deconstruct();
|
||||
self.validator_store
|
||||
.sign_block(*validator_pubkey, block, slot)
|
||||
.await
|
||||
.map(|b| SignedBlock::Full(PublishBlockRequest::new(Arc::new(b), maybe_blobs)))
|
||||
}
|
||||
UnsignedBlock::Blinded(block) => self
|
||||
.validator_store
|
||||
.sign_block(*validator_pubkey, block, slot)
|
||||
.await
|
||||
.map(Arc::new)
|
||||
.map(SignedBlock::Blinded),
|
||||
};
|
||||
|
||||
let signed_block = match res {
|
||||
Ok(block) => block,
|
||||
Err(ValidatorStoreError::UnknownPubkey(pubkey)) => {
|
||||
// A pubkey can be missing when a validator was recently removed
|
||||
// via the API.
|
||||
warn!(
|
||||
log,
|
||||
"Missing pubkey for block";
|
||||
"info" => "a validator may have recently been removed from this VC",
|
||||
"pubkey" => ?pubkey,
|
||||
"slot" => ?slot
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(BlockError::Recoverable(format!(
|
||||
"Unable to sign block: {:?}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let signing_time_ms =
|
||||
Duration::from_secs_f64(signing_timer.map_or(0.0, |t| t.stop_and_record())).as_millis();
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Publishing signed block";
|
||||
"slot" => slot.as_u64(),
|
||||
"signing_time_ms" => signing_time_ms,
|
||||
);
|
||||
|
||||
// Publish block with first available beacon node.
|
||||
//
|
||||
// Try the proposer nodes first, since we've likely gone to efforts to
|
||||
// protect them from DoS attacks and they're most likely to successfully
|
||||
// publish a block.
|
||||
proposer_fallback
|
||||
.request_proposers_first(|beacon_node| async {
|
||||
self.publish_signed_block_contents(&signed_block, beacon_node)
|
||||
.await
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Successfully published block";
|
||||
"block_type" => ?signed_block.block_type(),
|
||||
"deposits" => signed_block.num_deposits(),
|
||||
"attestations" => signed_block.num_attestations(),
|
||||
"graffiti" => ?graffiti.map(|g| g.as_utf8_lossy()),
|
||||
"slot" => signed_block.slot().as_u64(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish_block(
|
||||
self,
|
||||
slot: Slot,
|
||||
validator_pubkey: PublicKeyBytes,
|
||||
builder_boost_factor: Option<u64>,
|
||||
) -> Result<(), BlockError> {
|
||||
let log = self.context.log();
|
||||
let _timer =
|
||||
metrics::start_timer_vec(&metrics::BLOCK_SERVICE_TIMES, &[metrics::BEACON_BLOCK]);
|
||||
|
||||
let randao_reveal = match self
|
||||
.validator_store
|
||||
.randao_reveal(validator_pubkey, slot.epoch(E::slots_per_epoch()))
|
||||
.await
|
||||
{
|
||||
Ok(signature) => signature.into(),
|
||||
Err(ValidatorStoreError::UnknownPubkey(pubkey)) => {
|
||||
// A pubkey can be missing when a validator was recently removed
|
||||
// via the API.
|
||||
warn!(
|
||||
log,
|
||||
"Missing pubkey for block randao";
|
||||
"info" => "a validator may have recently been removed from this VC",
|
||||
"pubkey" => ?pubkey,
|
||||
"slot" => ?slot
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(BlockError::Recoverable(format!(
|
||||
"Unable to produce randao reveal signature: {:?}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let graffiti = determine_graffiti(
|
||||
&validator_pubkey,
|
||||
log,
|
||||
self.graffiti_file.clone(),
|
||||
self.validator_store.graffiti(&validator_pubkey),
|
||||
self.graffiti,
|
||||
);
|
||||
|
||||
let randao_reveal_ref = &randao_reveal;
|
||||
let self_ref = &self;
|
||||
let proposer_index = self.validator_store.validator_index(&validator_pubkey);
|
||||
let proposer_fallback = ProposerFallback {
|
||||
beacon_nodes: self.beacon_nodes.clone(),
|
||||
proposer_nodes: self.proposer_nodes.clone(),
|
||||
};
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Requesting unsigned block";
|
||||
"slot" => slot.as_u64(),
|
||||
);
|
||||
|
||||
// Request block from first responsive beacon node.
|
||||
//
|
||||
// Try the proposer nodes last, since it's likely that they don't have a
|
||||
// great view of attestations on the network.
|
||||
let unsigned_block = proposer_fallback
|
||||
.request_proposers_last(|beacon_node| async move {
|
||||
let _get_timer = metrics::start_timer_vec(
|
||||
&metrics::BLOCK_SERVICE_TIMES,
|
||||
&[metrics::BEACON_BLOCK_HTTP_GET],
|
||||
);
|
||||
Self::get_validator_block(
|
||||
&beacon_node,
|
||||
slot,
|
||||
randao_reveal_ref,
|
||||
graffiti,
|
||||
proposer_index,
|
||||
builder_boost_factor,
|
||||
log,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
BlockError::Recoverable(format!(
|
||||
"Error from beacon node when producing block: {:?}",
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
self_ref
|
||||
.sign_and_publish_block(
|
||||
proposer_fallback,
|
||||
slot,
|
||||
graffiti,
|
||||
&validator_pubkey,
|
||||
unsigned_block,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish_signed_block_contents(
|
||||
&self,
|
||||
signed_block: &SignedBlock<E>,
|
||||
beacon_node: BeaconNodeHttpClient,
|
||||
) -> Result<(), BlockError> {
|
||||
let log = self.context.log();
|
||||
let slot = signed_block.slot();
|
||||
match signed_block {
|
||||
SignedBlock::Full(signed_block) => {
|
||||
let _post_timer = metrics::start_timer_vec(
|
||||
&metrics::BLOCK_SERVICE_TIMES,
|
||||
&[metrics::BEACON_BLOCK_HTTP_POST],
|
||||
);
|
||||
beacon_node
|
||||
.post_beacon_blocks_v2_ssz(signed_block, None)
|
||||
.await
|
||||
.or_else(|e| handle_block_post_error(e, slot, log))?
|
||||
}
|
||||
SignedBlock::Blinded(signed_block) => {
|
||||
let _post_timer = metrics::start_timer_vec(
|
||||
&metrics::BLOCK_SERVICE_TIMES,
|
||||
&[metrics::BLINDED_BEACON_BLOCK_HTTP_POST],
|
||||
);
|
||||
beacon_node
|
||||
.post_beacon_blinded_blocks_v2_ssz(signed_block, None)
|
||||
.await
|
||||
.or_else(|e| handle_block_post_error(e, slot, log))?
|
||||
}
|
||||
}
|
||||
Ok::<_, BlockError>(())
|
||||
}
|
||||
|
||||
async fn get_validator_block(
|
||||
beacon_node: &BeaconNodeHttpClient,
|
||||
slot: Slot,
|
||||
randao_reveal_ref: &SignatureBytes,
|
||||
graffiti: Option<Graffiti>,
|
||||
proposer_index: Option<u64>,
|
||||
builder_boost_factor: Option<u64>,
|
||||
log: &Logger,
|
||||
) -> Result<UnsignedBlock<E>, BlockError> {
|
||||
let (block_response, _) = beacon_node
|
||||
.get_validator_blocks_v3::<E>(
|
||||
slot,
|
||||
randao_reveal_ref,
|
||||
graffiti.as_ref(),
|
||||
builder_boost_factor,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
BlockError::Recoverable(format!(
|
||||
"Error from beacon node when producing block: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let unsigned_block = match block_response.data {
|
||||
eth2::types::ProduceBlockV3Response::Full(block) => UnsignedBlock::Full(block),
|
||||
eth2::types::ProduceBlockV3Response::Blinded(block) => UnsignedBlock::Blinded(block),
|
||||
};
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Received unsigned block";
|
||||
"slot" => slot.as_u64(),
|
||||
);
|
||||
if proposer_index != Some(unsigned_block.proposer_index()) {
|
||||
return Err(BlockError::Recoverable(
|
||||
"Proposer index does not match block proposer. Beacon chain re-orged".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok::<_, BlockError>(unsigned_block)
|
||||
}
|
||||
|
||||
/// Returns the builder boost factor of the given public key.
|
||||
/// The priority order for fetching this value is:
|
||||
///
|
||||
/// 1. validator_definitions.yml
|
||||
/// 2. process level flag
|
||||
fn get_builder_boost_factor(&self, validator_pubkey: &PublicKeyBytes) -> Option<u64> {
|
||||
// Apply per validator configuration first.
|
||||
let validator_builder_boost_factor = self
|
||||
.validator_store
|
||||
.determine_validator_builder_boost_factor(validator_pubkey);
|
||||
|
||||
// Fallback to process-wide configuration if needed.
|
||||
let maybe_builder_boost_factor = validator_builder_boost_factor.or_else(|| {
|
||||
self.validator_store
|
||||
.determine_default_builder_boost_factor()
|
||||
});
|
||||
|
||||
if let Some(builder_boost_factor) = maybe_builder_boost_factor {
|
||||
// if builder boost factor is set to 100 it should be treated
|
||||
// as None to prevent unnecessary calculations that could
|
||||
// lead to loss of information.
|
||||
if builder_boost_factor == 100 {
|
||||
return None;
|
||||
}
|
||||
return Some(builder_boost_factor);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UnsignedBlock<E: EthSpec> {
|
||||
Full(FullBlockContents<E>),
|
||||
Blinded(BlindedBeaconBlock<E>),
|
||||
}
|
||||
|
||||
impl<E: EthSpec> UnsignedBlock<E> {
|
||||
pub fn proposer_index(&self) -> u64 {
|
||||
match self {
|
||||
UnsignedBlock::Full(block) => block.block().proposer_index(),
|
||||
UnsignedBlock::Blinded(block) => block.proposer_index(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SignedBlock<E: EthSpec> {
|
||||
Full(PublishBlockRequest<E>),
|
||||
Blinded(Arc<SignedBlindedBeaconBlock<E>>),
|
||||
}
|
||||
|
||||
impl<E: EthSpec> SignedBlock<E> {
|
||||
pub fn block_type(&self) -> BlockType {
|
||||
match self {
|
||||
SignedBlock::Full(_) => BlockType::Full,
|
||||
SignedBlock::Blinded(_) => BlockType::Blinded,
|
||||
}
|
||||
}
|
||||
pub fn slot(&self) -> Slot {
|
||||
match self {
|
||||
SignedBlock::Full(block) => block.signed_block().message().slot(),
|
||||
SignedBlock::Blinded(block) => block.message().slot(),
|
||||
}
|
||||
}
|
||||
pub fn num_deposits(&self) -> usize {
|
||||
match self {
|
||||
SignedBlock::Full(block) => block.signed_block().message().body().deposits().len(),
|
||||
SignedBlock::Blinded(block) => block.message().body().deposits().len(),
|
||||
}
|
||||
}
|
||||
pub fn num_attestations(&self) -> usize {
|
||||
match self {
|
||||
SignedBlock::Full(block) => block.signed_block().message().body().attestations_len(),
|
||||
SignedBlock::Blinded(block) => block.message().body().attestations_len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_block_post_error(err: eth2::Error, slot: Slot, log: &Logger) -> Result<(), BlockError> {
|
||||
// Handle non-200 success codes.
|
||||
if let Some(status) = err.status() {
|
||||
if status == StatusCode::ACCEPTED {
|
||||
info!(
|
||||
log,
|
||||
"Block is already known to BN or might be invalid";
|
||||
"slot" => slot,
|
||||
"status_code" => status.as_u16(),
|
||||
);
|
||||
return Ok(());
|
||||
} else if status.is_success() {
|
||||
debug!(
|
||||
log,
|
||||
"Block published with non-standard success code";
|
||||
"slot" => slot,
|
||||
"status_code" => status.as_u16(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(BlockError::Irrecoverable(format!(
|
||||
"Error from beacon node when publishing block: {err:?}",
|
||||
)))
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use crate::beacon_node_fallback::CandidateError;
|
||||
use eth2::{types::Slot, BeaconNodeHttpClient};
|
||||
use slog::{warn, Logger};
|
||||
|
||||
pub async fn check_node_health(
|
||||
beacon_node: &BeaconNodeHttpClient,
|
||||
log: &Logger,
|
||||
) -> Result<(Slot, bool, bool), CandidateError> {
|
||||
let resp = match beacon_node.get_node_syncing().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
log,
|
||||
"Unable connect to beacon node";
|
||||
"error" => %e
|
||||
);
|
||||
|
||||
return Err(CandidateError::Offline);
|
||||
}
|
||||
};
|
||||
|
||||
Ok((
|
||||
resp.data.head_slot,
|
||||
resp.data.is_optimistic,
|
||||
resp.data.el_offline,
|
||||
))
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
use crate::beacon_node_fallback::ApiTopic;
|
||||
use crate::graffiti_file::GraffitiFile;
|
||||
use crate::{
|
||||
beacon_node_fallback, beacon_node_health::BeaconNodeSyncDistanceTiers, http_api, http_metrics,
|
||||
};
|
||||
use beacon_node_fallback::{beacon_node_health::BeaconNodeSyncDistanceTiers, ApiTopic};
|
||||
use clap::ArgMatches;
|
||||
use clap_utils::{flags::DISABLE_MALLOC_TUNING_FLAG, parse_optional, parse_required};
|
||||
use directory::{
|
||||
@@ -10,6 +6,8 @@ use directory::{
|
||||
DEFAULT_VALIDATOR_DIR,
|
||||
};
|
||||
use eth2::types::Graffiti;
|
||||
use graffiti_file::GraffitiFile;
|
||||
use initialized_validators::Config as InitializedValidatorsConfig;
|
||||
use sensitive_url::SensitiveUrl;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use slog::{info, warn, Logger};
|
||||
@@ -19,13 +17,18 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use types::{Address, GRAFFITI_BYTES_LEN};
|
||||
use validator_http_api;
|
||||
use validator_http_metrics;
|
||||
use validator_store::Config as ValidatorStoreConfig;
|
||||
|
||||
pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/";
|
||||
pub const DEFAULT_WEB3SIGNER_KEEP_ALIVE: Option<Duration> = Some(Duration::from_secs(20));
|
||||
|
||||
/// Stores the core configuration for this validator instance.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Configuration parameters for the validator store.
|
||||
#[serde(flatten)]
|
||||
pub validator_store: ValidatorStoreConfig,
|
||||
/// The data directory, which stores all validator databases
|
||||
pub validator_dir: PathBuf,
|
||||
/// The directory containing the passwords to unlock validator keystores.
|
||||
@@ -49,12 +52,10 @@ pub struct Config {
|
||||
pub graffiti: Option<Graffiti>,
|
||||
/// Graffiti file to load per validator graffitis.
|
||||
pub graffiti_file: Option<GraffitiFile>,
|
||||
/// Fallback fallback address.
|
||||
pub fee_recipient: Option<Address>,
|
||||
/// Configuration for the HTTP REST API.
|
||||
pub http_api: http_api::Config,
|
||||
pub http_api: validator_http_api::Config,
|
||||
/// Configuration for the HTTP REST API.
|
||||
pub http_metrics: http_metrics::Config,
|
||||
pub http_metrics: validator_http_metrics::Config,
|
||||
/// Configuration for the Beacon Node fallback.
|
||||
pub beacon_node_fallback: beacon_node_fallback::Config,
|
||||
/// Configuration for sending metrics to a remote explorer endpoint.
|
||||
@@ -68,11 +69,7 @@ pub struct Config {
|
||||
/// (<= 64 validators)
|
||||
pub enable_high_validator_count_metrics: bool,
|
||||
/// Enable use of the blinded block endpoints during proposals.
|
||||
pub builder_proposals: bool,
|
||||
/// Overrides the timestamp field in builder api ValidatorRegistrationV1
|
||||
pub builder_registration_timestamp_override: Option<u64>,
|
||||
/// Fallback gas limit.
|
||||
pub gas_limit: Option<u64>,
|
||||
/// A list of custom certificates that the validator client will additionally use when
|
||||
/// connecting to a beacon node over SSL/TLS.
|
||||
pub beacon_nodes_tls_certs: Option<Vec<PathBuf>>,
|
||||
@@ -82,16 +79,11 @@ pub struct Config {
|
||||
pub enable_latency_measurement_service: bool,
|
||||
/// Defines the number of validators per `validator/register_validator` request sent to the BN.
|
||||
pub validator_registration_batch_size: usize,
|
||||
/// Enable slashing protection even while using web3signer keys.
|
||||
pub enable_web3signer_slashing_protection: bool,
|
||||
/// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value.
|
||||
pub builder_boost_factor: Option<u64>,
|
||||
/// If true, Lighthouse will prefer builder proposals, if available.
|
||||
pub prefer_builder_proposals: bool,
|
||||
/// Whether we are running with distributed network support.
|
||||
pub distributed: bool,
|
||||
pub web3_signer_keep_alive_timeout: Option<Duration>,
|
||||
pub web3_signer_max_idle_connections: Option<usize>,
|
||||
/// Configuration for the initialized validators
|
||||
#[serde(flatten)]
|
||||
pub initialized_validators: InitializedValidatorsConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -109,6 +101,7 @@ impl Default for Config {
|
||||
let beacon_nodes = vec![SensitiveUrl::parse(DEFAULT_BEACON_NODE)
|
||||
.expect("beacon_nodes must always be a valid url.")];
|
||||
Self {
|
||||
validator_store: ValidatorStoreConfig::default(),
|
||||
validator_dir,
|
||||
secrets_dir,
|
||||
beacon_nodes,
|
||||
@@ -119,7 +112,6 @@ impl Default for Config {
|
||||
use_long_timeouts: false,
|
||||
graffiti: None,
|
||||
graffiti_file: None,
|
||||
fee_recipient: None,
|
||||
http_api: <_>::default(),
|
||||
http_metrics: <_>::default(),
|
||||
beacon_node_fallback: <_>::default(),
|
||||
@@ -127,18 +119,12 @@ impl Default for Config {
|
||||
enable_doppelganger_protection: false,
|
||||
enable_high_validator_count_metrics: false,
|
||||
beacon_nodes_tls_certs: None,
|
||||
builder_proposals: false,
|
||||
builder_registration_timestamp_override: None,
|
||||
gas_limit: None,
|
||||
broadcast_topics: vec![ApiTopic::Subscriptions],
|
||||
enable_latency_measurement_service: true,
|
||||
validator_registration_batch_size: 500,
|
||||
enable_web3signer_slashing_protection: true,
|
||||
builder_boost_factor: None,
|
||||
prefer_builder_proposals: false,
|
||||
distributed: false,
|
||||
web3_signer_keep_alive_timeout: DEFAULT_WEB3SIGNER_KEEP_ALIVE,
|
||||
web3_signer_max_idle_connections: None,
|
||||
initialized_validators: <_>::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +219,7 @@ impl Config {
|
||||
if let Some(input_fee_recipient) =
|
||||
parse_optional::<Address>(cli_args, "suggested-fee-recipient")?
|
||||
{
|
||||
config.fee_recipient = Some(input_fee_recipient);
|
||||
config.validator_store.fee_recipient = Some(input_fee_recipient);
|
||||
}
|
||||
|
||||
if let Some(tls_certs) = parse_optional::<String>(cli_args, "beacon-nodes-tls-certs")? {
|
||||
@@ -270,7 +256,7 @@ impl Config {
|
||||
* Web3 signer
|
||||
*/
|
||||
if let Some(s) = parse_optional::<String>(cli_args, "web3-signer-keep-alive-timeout")? {
|
||||
config.web3_signer_keep_alive_timeout = if s == "null" {
|
||||
config.initialized_validators.web3_signer_keep_alive_timeout = if s == "null" {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_millis(
|
||||
@@ -279,7 +265,9 @@ impl Config {
|
||||
}
|
||||
}
|
||||
if let Some(n) = parse_optional::<usize>(cli_args, "web3-signer-max-idle-connections")? {
|
||||
config.web3_signer_max_idle_connections = Some(n);
|
||||
config
|
||||
.initialized_validators
|
||||
.web3_signer_max_idle_connections = Some(n);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -382,14 +370,14 @@ impl Config {
|
||||
}
|
||||
|
||||
if cli_args.get_flag("builder-proposals") {
|
||||
config.builder_proposals = true;
|
||||
config.validator_store.builder_proposals = true;
|
||||
}
|
||||
|
||||
if cli_args.get_flag("prefer-builder-proposals") {
|
||||
config.prefer_builder_proposals = true;
|
||||
config.validator_store.prefer_builder_proposals = true;
|
||||
}
|
||||
|
||||
config.gas_limit = cli_args
|
||||
config.validator_store.gas_limit = cli_args
|
||||
.get_one::<String>("gas-limit")
|
||||
.map(|gas_limit| {
|
||||
gas_limit
|
||||
@@ -408,7 +396,8 @@ impl Config {
|
||||
);
|
||||
}
|
||||
|
||||
config.builder_boost_factor = parse_optional(cli_args, "builder-boost-factor")?;
|
||||
config.validator_store.builder_boost_factor =
|
||||
parse_optional(cli_args, "builder-boost-factor")?;
|
||||
|
||||
config.enable_latency_measurement_service =
|
||||
!cli_args.get_flag("disable-latency-measurement-service");
|
||||
@@ -419,7 +408,7 @@ impl Config {
|
||||
return Err("validator-registration-batch-size cannot be 0".to_string());
|
||||
}
|
||||
|
||||
config.enable_web3signer_slashing_protection =
|
||||
config.validator_store.enable_web3signer_slashing_protection =
|
||||
if cli_args.get_flag("disable-slashing-protection-web3signer") {
|
||||
warn!(
|
||||
log,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,653 +0,0 @@
|
||||
use crate::{
|
||||
doppelganger_service::DoppelgangerStatus,
|
||||
duties_service::{DutiesService, Error},
|
||||
http_metrics::metrics,
|
||||
validator_store::Error as ValidatorStoreError,
|
||||
};
|
||||
|
||||
use futures::future::join_all;
|
||||
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use slog::{crit, debug, info, warn};
|
||||
use slot_clock::SlotClock;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use types::{ChainSpec, EthSpec, PublicKeyBytes, Slot, SyncDuty, SyncSelectionProof, SyncSubnetId};
|
||||
|
||||
/// Number of epochs in advance to compute selection proofs when not in `distributed` mode.
|
||||
pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2;
|
||||
/// Number of slots in advance to compute selection proofs when in `distributed` mode.
|
||||
pub const AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED: u64 = 1;
|
||||
|
||||
/// Top-level data-structure containing sync duty information.
|
||||
///
|
||||
/// This data is structured as a series of nested `HashMap`s wrapped in `RwLock`s. Fine-grained
|
||||
/// locking is used to provide maximum concurrency for the different services reading and writing.
|
||||
///
|
||||
/// Deadlocks are prevented by:
|
||||
///
|
||||
/// 1. Hierarchical locking. It is impossible to lock an inner lock (e.g. `validators`) without
|
||||
/// first locking its parent.
|
||||
/// 2. One-at-a-time locking. For the innermost locks on the aggregator duties, all of the functions
|
||||
/// in this file take care to only lock one validator at a time. We never hold a lock while
|
||||
/// trying to obtain another one (hence no lock ordering issues).
|
||||
pub struct SyncDutiesMap<E: EthSpec> {
|
||||
/// Map from sync committee period to duties for members of that sync committee.
|
||||
committees: RwLock<HashMap<u64, CommitteeDuties>>,
|
||||
/// Whether we are in `distributed` mode and using reduced lookahead for aggregate pre-compute.
|
||||
distributed: bool,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
/// Duties for a single sync committee period.
|
||||
#[derive(Default)]
|
||||
pub struct CommitteeDuties {
|
||||
/// Map from validator index to validator duties.
|
||||
///
|
||||
/// A `None` value indicates that the validator index is known *not* to be a member of the sync
|
||||
/// committee, while a `Some` indicates a known member. An absent value indicates that the
|
||||
/// validator index was not part of the set of local validators when the duties were fetched.
|
||||
/// This allows us to track changes to the set of local validators.
|
||||
validators: RwLock<HashMap<u64, Option<ValidatorDuties>>>,
|
||||
}
|
||||
|
||||
/// Duties for a single validator.
|
||||
pub struct ValidatorDuties {
|
||||
/// The sync duty: including validator sync committee indices & pubkey.
|
||||
duty: SyncDuty,
|
||||
/// The aggregator duties: cached selection proofs for upcoming epochs.
|
||||
aggregation_duties: AggregatorDuties,
|
||||
}
|
||||
|
||||
/// Aggregator duties for a single validator.
|
||||
pub struct AggregatorDuties {
|
||||
/// The slot up to which aggregation proofs have already been computed (inclusive).
|
||||
pre_compute_slot: RwLock<Option<Slot>>,
|
||||
/// Map from slot & subnet ID to proof that this validator is an aggregator.
|
||||
///
|
||||
/// The slot is the slot at which the signed contribution and proof should be broadcast,
|
||||
/// which is 1 less than the slot for which the `duty` was computed.
|
||||
proofs: RwLock<HashMap<(Slot, SyncSubnetId), SyncSelectionProof>>,
|
||||
}
|
||||
|
||||
/// Duties for multiple validators, for a single slot.
|
||||
///
|
||||
/// This type is returned to the sync service.
|
||||
pub struct SlotDuties {
|
||||
/// List of duties for all sync committee members at this slot.
|
||||
///
|
||||
/// Note: this is intentionally NOT split by subnet so that we only sign
|
||||
/// one `SyncCommitteeMessage` per validator (recall a validator may be part of multiple
|
||||
/// subnets).
|
||||
pub duties: Vec<SyncDuty>,
|
||||
/// Map from subnet ID to validator index and selection proof of each aggregator.
|
||||
pub aggregators: HashMap<SyncSubnetId, Vec<(u64, PublicKeyBytes, SyncSelectionProof)>>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> SyncDutiesMap<E> {
|
||||
pub fn new(distributed: bool) -> Self {
|
||||
Self {
|
||||
committees: RwLock::new(HashMap::new()),
|
||||
distributed,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if duties are already known for all of the given validators for `committee_period`.
|
||||
fn all_duties_known(&self, committee_period: u64, validator_indices: &[u64]) -> bool {
|
||||
self.committees
|
||||
.read()
|
||||
.get(&committee_period)
|
||||
.map_or(false, |committee_duties| {
|
||||
let validator_duties = committee_duties.validators.read();
|
||||
validator_indices
|
||||
.iter()
|
||||
.all(|index| validator_duties.contains_key(index))
|
||||
})
|
||||
}
|
||||
|
||||
/// Number of slots in advance to compute selection proofs
|
||||
fn aggregation_pre_compute_slots(&self) -> u64 {
|
||||
if self.distributed {
|
||||
AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED
|
||||
} else {
|
||||
E::slots_per_epoch() * AGGREGATION_PRE_COMPUTE_EPOCHS
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare for pre-computation of selection proofs for `committee_period`.
|
||||
///
|
||||
/// Return the slot up to which proofs should be pre-computed, as well as a vec of
|
||||
/// `(previous_pre_compute_slot, sync_duty)` pairs for all validators which need to have proofs
|
||||
/// computed. See `fill_in_aggregation_proofs` for the actual calculation.
|
||||
fn prepare_for_aggregator_pre_compute(
|
||||
&self,
|
||||
committee_period: u64,
|
||||
current_slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> (Slot, Vec<(Slot, SyncDuty)>) {
|
||||
let default_start_slot = std::cmp::max(
|
||||
current_slot,
|
||||
first_slot_of_period::<E>(committee_period, spec),
|
||||
);
|
||||
let pre_compute_lookahead_slots = self.aggregation_pre_compute_slots();
|
||||
let pre_compute_slot = std::cmp::min(
|
||||
current_slot + pre_compute_lookahead_slots,
|
||||
last_slot_of_period::<E>(committee_period, spec),
|
||||
);
|
||||
|
||||
let pre_compute_duties = self.committees.read().get(&committee_period).map_or_else(
|
||||
Vec::new,
|
||||
|committee_duties| {
|
||||
let validator_duties = committee_duties.validators.read();
|
||||
validator_duties
|
||||
.values()
|
||||
.filter_map(|maybe_duty| {
|
||||
let duty = maybe_duty.as_ref()?;
|
||||
let old_pre_compute_slot = duty
|
||||
.aggregation_duties
|
||||
.pre_compute_slot
|
||||
.write()
|
||||
.replace(pre_compute_slot);
|
||||
|
||||
match old_pre_compute_slot {
|
||||
// No proofs pre-computed previously, compute all from the start of
|
||||
// the period or the current slot (whichever is later).
|
||||
None => Some((default_start_slot, duty.duty.clone())),
|
||||
// Proofs computed up to `prev`, start from the subsequent epoch.
|
||||
Some(prev) if prev < pre_compute_slot => {
|
||||
Some((prev + 1, duty.duty.clone()))
|
||||
}
|
||||
// Proofs already known, no need to compute.
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
(pre_compute_slot, pre_compute_duties)
|
||||
}
|
||||
|
||||
fn get_or_create_committee_duties<'a, 'b>(
|
||||
&'a self,
|
||||
committee_period: u64,
|
||||
validator_indices: impl IntoIterator<Item = &'b u64>,
|
||||
) -> MappedRwLockReadGuard<'a, CommitteeDuties> {
|
||||
let mut committees_writer = self.committees.write();
|
||||
|
||||
committees_writer
|
||||
.entry(committee_period)
|
||||
.or_default()
|
||||
.init(validator_indices);
|
||||
|
||||
// Return shared reference
|
||||
RwLockReadGuard::map(
|
||||
RwLockWriteGuard::downgrade(committees_writer),
|
||||
|committees_reader| &committees_reader[&committee_period],
|
||||
)
|
||||
}
|
||||
|
||||
/// Get duties for all validators for the given `wall_clock_slot`.
|
||||
///
|
||||
/// This is the entry-point for the sync committee service.
|
||||
pub fn get_duties_for_slot(
|
||||
&self,
|
||||
wall_clock_slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Option<SlotDuties> {
|
||||
// Sync duties lag their assigned slot by 1
|
||||
let duty_slot = wall_clock_slot + 1;
|
||||
|
||||
let sync_committee_period = duty_slot
|
||||
.epoch(E::slots_per_epoch())
|
||||
.sync_committee_period(spec)
|
||||
.ok()?;
|
||||
|
||||
let committees_reader = self.committees.read();
|
||||
let committee_duties = committees_reader.get(&sync_committee_period)?;
|
||||
|
||||
let mut duties = vec![];
|
||||
let mut aggregators = HashMap::new();
|
||||
|
||||
committee_duties
|
||||
.validators
|
||||
.read()
|
||||
.values()
|
||||
// Filter out non-members & failed subnet IDs.
|
||||
.filter_map(|opt_duties| {
|
||||
let duty = opt_duties.as_ref()?;
|
||||
let subnet_ids = duty.duty.subnet_ids::<E>().ok()?;
|
||||
Some((duty, subnet_ids))
|
||||
})
|
||||
// Add duties for members to the vec of all duties, and aggregators to the
|
||||
// aggregators map.
|
||||
.for_each(|(validator_duty, subnet_ids)| {
|
||||
duties.push(validator_duty.duty.clone());
|
||||
|
||||
let proofs = validator_duty.aggregation_duties.proofs.read();
|
||||
|
||||
for subnet_id in subnet_ids {
|
||||
if let Some(proof) = proofs.get(&(wall_clock_slot, subnet_id)) {
|
||||
aggregators.entry(subnet_id).or_insert_with(Vec::new).push((
|
||||
validator_duty.duty.validator_index,
|
||||
validator_duty.duty.pubkey,
|
||||
proof.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Some(SlotDuties {
|
||||
duties,
|
||||
aggregators,
|
||||
})
|
||||
}
|
||||
|
||||
/// Prune duties for past sync committee periods from the map.
|
||||
fn prune(&self, current_sync_committee_period: u64) {
|
||||
self.committees
|
||||
.write()
|
||||
.retain(|period, _| *period >= current_sync_committee_period)
|
||||
}
|
||||
}
|
||||
|
||||
impl CommitteeDuties {
|
||||
fn init<'b>(&mut self, validator_indices: impl IntoIterator<Item = &'b u64>) {
|
||||
validator_indices.into_iter().for_each(|validator_index| {
|
||||
self.validators
|
||||
.get_mut()
|
||||
.entry(*validator_index)
|
||||
.or_insert(None);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidatorDuties {
|
||||
fn new(duty: SyncDuty) -> Self {
|
||||
Self {
|
||||
duty,
|
||||
aggregation_duties: AggregatorDuties {
|
||||
pre_compute_slot: RwLock::new(None),
|
||||
proofs: RwLock::new(HashMap::new()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of epochs to wait from the start of the period before actually fetching duties.
|
||||
fn epoch_offset(spec: &ChainSpec) -> u64 {
|
||||
spec.epochs_per_sync_committee_period.as_u64() / 2
|
||||
}
|
||||
|
||||
fn first_slot_of_period<E: EthSpec>(sync_committee_period: u64, spec: &ChainSpec) -> Slot {
|
||||
(spec.epochs_per_sync_committee_period * sync_committee_period).start_slot(E::slots_per_epoch())
|
||||
}
|
||||
|
||||
fn last_slot_of_period<E: EthSpec>(sync_committee_period: u64, spec: &ChainSpec) -> Slot {
|
||||
first_slot_of_period::<E>(sync_committee_period + 1, spec) - 1
|
||||
}
|
||||
|
||||
pub async fn poll_sync_committee_duties<T: SlotClock + 'static, E: EthSpec>(
|
||||
duties_service: &Arc<DutiesService<T, E>>,
|
||||
) -> Result<(), Error> {
|
||||
let sync_duties = &duties_service.sync_duties;
|
||||
let spec = &duties_service.spec;
|
||||
let current_slot = duties_service
|
||||
.slot_clock
|
||||
.now()
|
||||
.ok_or(Error::UnableToReadSlotClock)?;
|
||||
let current_epoch = current_slot.epoch(E::slots_per_epoch());
|
||||
|
||||
// If the Altair fork is yet to be activated, do not attempt to poll for duties.
|
||||
if spec
|
||||
.altair_fork_epoch
|
||||
.map_or(true, |altair_epoch| current_epoch < altair_epoch)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let current_sync_committee_period = current_epoch.sync_committee_period(spec)?;
|
||||
let next_sync_committee_period = current_sync_committee_period + 1;
|
||||
|
||||
// Collect *all* pubkeys, even those undergoing doppelganger protection.
|
||||
//
|
||||
// Sync committee messages are not slashable and are currently excluded from doppelganger
|
||||
// protection.
|
||||
let local_pubkeys: HashSet<_> = duties_service
|
||||
.validator_store
|
||||
.voting_pubkeys(DoppelgangerStatus::ignored);
|
||||
|
||||
let local_indices = {
|
||||
let mut local_indices = Vec::with_capacity(local_pubkeys.len());
|
||||
|
||||
let vals_ref = duties_service.validator_store.initialized_validators();
|
||||
let vals = vals_ref.read();
|
||||
for &pubkey in &local_pubkeys {
|
||||
if let Some(validator_index) = vals.get_index(&pubkey) {
|
||||
local_indices.push(validator_index)
|
||||
}
|
||||
}
|
||||
local_indices
|
||||
};
|
||||
|
||||
// If duties aren't known for the current period, poll for them.
|
||||
if !sync_duties.all_duties_known(current_sync_committee_period, &local_indices) {
|
||||
poll_sync_committee_duties_for_period(
|
||||
duties_service,
|
||||
&local_indices,
|
||||
current_sync_committee_period,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Prune previous duties (we avoid doing this too often as it locks the whole map).
|
||||
sync_duties.prune(current_sync_committee_period);
|
||||
}
|
||||
|
||||
// Pre-compute aggregator selection proofs for the current period.
|
||||
let (current_pre_compute_slot, new_pre_compute_duties) = sync_duties
|
||||
.prepare_for_aggregator_pre_compute(current_sync_committee_period, current_slot, spec);
|
||||
|
||||
if !new_pre_compute_duties.is_empty() {
|
||||
let sub_duties_service = duties_service.clone();
|
||||
duties_service.context.executor.spawn(
|
||||
async move {
|
||||
fill_in_aggregation_proofs(
|
||||
sub_duties_service,
|
||||
&new_pre_compute_duties,
|
||||
current_sync_committee_period,
|
||||
current_slot,
|
||||
current_pre_compute_slot,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"duties_service_sync_selection_proofs",
|
||||
);
|
||||
}
|
||||
|
||||
// If we're past the point in the current period where we should determine duties for the next
|
||||
// period and they are not yet known, then poll.
|
||||
if current_epoch.as_u64() % spec.epochs_per_sync_committee_period.as_u64() >= epoch_offset(spec)
|
||||
&& !sync_duties.all_duties_known(next_sync_committee_period, &local_indices)
|
||||
{
|
||||
poll_sync_committee_duties_for_period(
|
||||
duties_service,
|
||||
&local_indices,
|
||||
next_sync_committee_period,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Prune (this is the main code path for updating duties, so we should almost always hit
|
||||
// this prune).
|
||||
sync_duties.prune(current_sync_committee_period);
|
||||
}
|
||||
|
||||
// Pre-compute aggregator selection proofs for the next period.
|
||||
let aggregate_pre_compute_lookahead_slots = sync_duties.aggregation_pre_compute_slots();
|
||||
if (current_slot + aggregate_pre_compute_lookahead_slots)
|
||||
.epoch(E::slots_per_epoch())
|
||||
.sync_committee_period(spec)?
|
||||
== next_sync_committee_period
|
||||
{
|
||||
let (pre_compute_slot, new_pre_compute_duties) = sync_duties
|
||||
.prepare_for_aggregator_pre_compute(next_sync_committee_period, current_slot, spec);
|
||||
|
||||
if !new_pre_compute_duties.is_empty() {
|
||||
let sub_duties_service = duties_service.clone();
|
||||
duties_service.context.executor.spawn(
|
||||
async move {
|
||||
fill_in_aggregation_proofs(
|
||||
sub_duties_service,
|
||||
&new_pre_compute_duties,
|
||||
next_sync_committee_period,
|
||||
current_slot,
|
||||
pre_compute_slot,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"duties_service_sync_selection_proofs",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn poll_sync_committee_duties_for_period<T: SlotClock + 'static, E: EthSpec>(
|
||||
duties_service: &Arc<DutiesService<T, E>>,
|
||||
local_indices: &[u64],
|
||||
sync_committee_period: u64,
|
||||
) -> Result<(), Error> {
|
||||
let spec = &duties_service.spec;
|
||||
let log = duties_service.context.log();
|
||||
|
||||
// no local validators don't need to poll for sync committee
|
||||
if local_indices.is_empty() {
|
||||
debug!(
|
||||
duties_service.context.log(),
|
||||
"No validators, not polling for sync committee duties";
|
||||
"sync_committee_period" => sync_committee_period,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!(
|
||||
log,
|
||||
"Fetching sync committee duties";
|
||||
"sync_committee_period" => sync_committee_period,
|
||||
"num_validators" => local_indices.len(),
|
||||
);
|
||||
|
||||
let period_start_epoch = spec.epochs_per_sync_committee_period * sync_committee_period;
|
||||
|
||||
let duties_response = duties_service
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::DUTIES_SERVICE_TIMES,
|
||||
&[metrics::VALIDATOR_DUTIES_SYNC_HTTP_POST],
|
||||
);
|
||||
beacon_node
|
||||
.post_validator_duties_sync(period_start_epoch, local_indices)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
let duties = match duties_response {
|
||||
Ok(res) => res.data,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
log,
|
||||
"Failed to download sync committee duties";
|
||||
"sync_committee_period" => sync_committee_period,
|
||||
"error" => %e,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
debug!(log, "Fetched sync duties from BN"; "count" => duties.len());
|
||||
|
||||
// Add duties to map.
|
||||
let committee_duties = duties_service
|
||||
.sync_duties
|
||||
.get_or_create_committee_duties(sync_committee_period, local_indices);
|
||||
|
||||
let mut validator_writer = committee_duties.validators.write();
|
||||
for duty in duties {
|
||||
let validator_duties = validator_writer
|
||||
.get_mut(&duty.validator_index)
|
||||
.ok_or(Error::SyncDutiesNotFound(duty.validator_index))?;
|
||||
|
||||
let updated = validator_duties.as_ref().map_or(true, |existing_duties| {
|
||||
let updated_due_to_reorg = existing_duties.duty.validator_sync_committee_indices
|
||||
!= duty.validator_sync_committee_indices;
|
||||
if updated_due_to_reorg {
|
||||
warn!(
|
||||
log,
|
||||
"Sync committee duties changed";
|
||||
"message" => "this could be due to a really long re-org, or a bug"
|
||||
);
|
||||
}
|
||||
updated_due_to_reorg
|
||||
});
|
||||
|
||||
if updated {
|
||||
info!(
|
||||
log,
|
||||
"Validator in sync committee";
|
||||
"validator_index" => duty.validator_index,
|
||||
"sync_committee_period" => sync_committee_period,
|
||||
);
|
||||
|
||||
*validator_duties = Some(ValidatorDuties::new(duty));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fill_in_aggregation_proofs<T: SlotClock + 'static, E: EthSpec>(
|
||||
duties_service: Arc<DutiesService<T, E>>,
|
||||
pre_compute_duties: &[(Slot, SyncDuty)],
|
||||
sync_committee_period: u64,
|
||||
current_slot: Slot,
|
||||
pre_compute_slot: Slot,
|
||||
) {
|
||||
let log = duties_service.context.log();
|
||||
|
||||
debug!(
|
||||
log,
|
||||
"Calculating sync selection proofs";
|
||||
"period" => sync_committee_period,
|
||||
"current_slot" => current_slot,
|
||||
"pre_compute_slot" => pre_compute_slot
|
||||
);
|
||||
|
||||
// Generate selection proofs for each validator at each slot, one slot at a time.
|
||||
for slot in (current_slot.as_u64()..=pre_compute_slot.as_u64()).map(Slot::new) {
|
||||
let mut validator_proofs = vec![];
|
||||
for (validator_start_slot, duty) in pre_compute_duties {
|
||||
// Proofs are already known at this slot for this validator.
|
||||
if slot < *validator_start_slot {
|
||||
continue;
|
||||
}
|
||||
|
||||
let subnet_ids = match duty.subnet_ids::<E>() {
|
||||
Ok(subnet_ids) => subnet_ids,
|
||||
Err(e) => {
|
||||
crit!(
|
||||
log,
|
||||
"Arithmetic error computing subnet IDs";
|
||||
"error" => ?e,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Create futures to produce proofs.
|
||||
let duties_service_ref = &duties_service;
|
||||
let futures = subnet_ids.iter().map(|subnet_id| async move {
|
||||
// Construct proof for prior slot.
|
||||
let proof_slot = slot - 1;
|
||||
|
||||
let proof = match duties_service_ref
|
||||
.validator_store
|
||||
.produce_sync_selection_proof(&duty.pubkey, proof_slot, *subnet_id)
|
||||
.await
|
||||
{
|
||||
Ok(proof) => proof,
|
||||
Err(ValidatorStoreError::UnknownPubkey(pubkey)) => {
|
||||
// A pubkey can be missing when a validator was recently
|
||||
// removed via the API.
|
||||
debug!(
|
||||
log,
|
||||
"Missing pubkey for sync selection proof";
|
||||
"pubkey" => ?pubkey,
|
||||
"pubkey" => ?duty.pubkey,
|
||||
"slot" => proof_slot,
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
log,
|
||||
"Unable to sign selection proof";
|
||||
"error" => ?e,
|
||||
"pubkey" => ?duty.pubkey,
|
||||
"slot" => proof_slot,
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match proof.is_aggregator::<E>() {
|
||||
Ok(true) => {
|
||||
debug!(
|
||||
log,
|
||||
"Validator is sync aggregator";
|
||||
"validator_index" => duty.validator_index,
|
||||
"slot" => proof_slot,
|
||||
"subnet_id" => %subnet_id,
|
||||
);
|
||||
Some(((proof_slot, *subnet_id), proof))
|
||||
}
|
||||
Ok(false) => None,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
log,
|
||||
"Error determining is_aggregator";
|
||||
"pubkey" => ?duty.pubkey,
|
||||
"slot" => proof_slot,
|
||||
"error" => ?e,
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let proofs = join_all(futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
validator_proofs.push((duty.validator_index, proofs));
|
||||
}
|
||||
|
||||
// Add to global storage (we add regularly so the proofs can be used ASAP).
|
||||
let sync_map = duties_service.sync_duties.committees.read();
|
||||
let Some(committee_duties) = sync_map.get(&sync_committee_period) else {
|
||||
debug!(
|
||||
log,
|
||||
"Missing sync duties";
|
||||
"period" => sync_committee_period,
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let validators = committee_duties.validators.read();
|
||||
let num_validators_updated = validator_proofs.len();
|
||||
|
||||
for (validator_index, proofs) in validator_proofs {
|
||||
if let Some(Some(duty)) = validators.get(&validator_index) {
|
||||
duty.aggregation_duties.proofs.write().extend(proofs);
|
||||
} else {
|
||||
debug!(
|
||||
log,
|
||||
"Missing sync duty to update";
|
||||
"validator_index" => validator_index,
|
||||
"period" => sync_committee_period,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if num_validators_updated > 0 {
|
||||
debug!(
|
||||
log,
|
||||
"Finished computing sync selection proofs";
|
||||
"slot" => slot,
|
||||
"updated_validators" => num_validators_updated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{prelude::*, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bls::PublicKeyBytes;
|
||||
use types::{graffiti::GraffitiString, Graffiti};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum Error {
|
||||
InvalidFile(std::io::Error),
|
||||
InvalidLine(String),
|
||||
InvalidPublicKey(String),
|
||||
InvalidGraffiti(String),
|
||||
}
|
||||
|
||||
/// Struct to load validator graffitis from file.
|
||||
/// The graffiti file is expected to have the following structure
|
||||
///
|
||||
/// default: Lighthouse
|
||||
/// public_key1: graffiti1
|
||||
/// public_key2: graffiti2
|
||||
/// ...
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GraffitiFile {
|
||||
graffiti_path: PathBuf,
|
||||
graffitis: HashMap<PublicKeyBytes, Graffiti>,
|
||||
default: Option<Graffiti>,
|
||||
}
|
||||
|
||||
impl GraffitiFile {
|
||||
pub fn new(graffiti_path: PathBuf) -> Self {
|
||||
Self {
|
||||
graffiti_path,
|
||||
graffitis: HashMap::new(),
|
||||
default: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the graffiti file and populates the default graffiti and `graffitis` hashmap.
|
||||
/// Returns the graffiti corresponding to the given public key if present, else returns the
|
||||
/// default graffiti.
|
||||
///
|
||||
/// Returns an error if loading from the graffiti file fails.
|
||||
pub fn load_graffiti(
|
||||
&mut self,
|
||||
public_key: &PublicKeyBytes,
|
||||
) -> Result<Option<Graffiti>, Error> {
|
||||
self.read_graffiti_file()?;
|
||||
Ok(self.graffitis.get(public_key).copied().or(self.default))
|
||||
}
|
||||
|
||||
/// Reads from a graffiti file with the specified format and populates the default value
|
||||
/// and the hashmap.
|
||||
///
|
||||
/// Returns an error if the file does not exist, or if the format is invalid.
|
||||
pub fn read_graffiti_file(&mut self) -> Result<(), Error> {
|
||||
let file = File::open(self.graffiti_path.as_path()).map_err(Error::InvalidFile)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let lines = reader.lines();
|
||||
|
||||
for line in lines {
|
||||
let line = line.map_err(|e| Error::InvalidLine(e.to_string()))?;
|
||||
let (pk_opt, graffiti) = read_line(&line)?;
|
||||
match pk_opt {
|
||||
Some(pk) => {
|
||||
self.graffitis.insert(pk, graffiti);
|
||||
}
|
||||
None => self.default = Some(graffiti),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a line from the graffiti file.
|
||||
///
|
||||
/// `Ok((None, graffiti))` represents the graffiti for the default key.
|
||||
/// `Ok((Some(pk), graffiti))` represents graffiti for the public key `pk`.
|
||||
/// Returns an error if the line is in the wrong format or does not contain a valid public key or graffiti.
|
||||
fn read_line(line: &str) -> Result<(Option<PublicKeyBytes>, Graffiti), Error> {
|
||||
if let Some(i) = line.find(':') {
|
||||
let (key, value) = line.split_at(i);
|
||||
// Note: `value.len() >=1` so `value[1..]` is safe
|
||||
let graffiti = GraffitiString::from_str(value[1..].trim())
|
||||
.map_err(Error::InvalidGraffiti)?
|
||||
.into();
|
||||
if key == "default" {
|
||||
Ok((None, graffiti))
|
||||
} else {
|
||||
let pk = PublicKeyBytes::from_str(key).map_err(Error::InvalidPublicKey)?;
|
||||
Ok((Some(pk), graffiti))
|
||||
}
|
||||
} else {
|
||||
Err(Error::InvalidLine(format!("Missing delimiter: {}", line)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bls::Keypair;
|
||||
use std::io::LineWriter;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const DEFAULT_GRAFFITI: &str = "lighthouse";
|
||||
const CUSTOM_GRAFFITI1: &str = "custom-graffiti1";
|
||||
const CUSTOM_GRAFFITI2: &str = "graffitiwall:720:641:#ffff00";
|
||||
const EMPTY_GRAFFITI: &str = "";
|
||||
const PK1: &str = "0x800012708dc03f611751aad7a43a082142832b5c1aceed07ff9b543cf836381861352aa923c70eeb02018b638aa306aa";
|
||||
const PK2: &str = "0x80001866ce324de7d80ec73be15e2d064dcf121adf1b34a0d679f2b9ecbab40ce021e03bb877e1a2fe72eaaf475e6e21";
|
||||
const PK3: &str = "0x9035d41a8bc11b08c17d0d93d876087958c9d055afe86fce558e3b988d92434769c8d50b0b463708db80c6aae1160c02";
|
||||
|
||||
// Create a graffiti file in the required format and return a path to the file.
|
||||
fn create_graffiti_file() -> PathBuf {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let pk1 = PublicKeyBytes::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap();
|
||||
let pk2 = PublicKeyBytes::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap();
|
||||
let pk3 = PublicKeyBytes::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap();
|
||||
|
||||
let file_name = temp.into_path().join("graffiti.txt");
|
||||
|
||||
let file = File::create(&file_name).unwrap();
|
||||
let mut graffiti_file = LineWriter::new(file);
|
||||
graffiti_file
|
||||
.write_all(format!("default: {}\n", DEFAULT_GRAFFITI).as_bytes())
|
||||
.unwrap();
|
||||
graffiti_file
|
||||
.write_all(format!("{}: {}\n", pk1.as_hex_string(), CUSTOM_GRAFFITI1).as_bytes())
|
||||
.unwrap();
|
||||
graffiti_file
|
||||
.write_all(format!("{}: {}\n", pk2.as_hex_string(), CUSTOM_GRAFFITI2).as_bytes())
|
||||
.unwrap();
|
||||
graffiti_file
|
||||
.write_all(format!("{}:{}\n", pk3.as_hex_string(), EMPTY_GRAFFITI).as_bytes())
|
||||
.unwrap();
|
||||
graffiti_file.flush().unwrap();
|
||||
file_name
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_graffiti() {
|
||||
let graffiti_file_path = create_graffiti_file();
|
||||
let mut gf = GraffitiFile::new(graffiti_file_path);
|
||||
|
||||
let pk1 = PublicKeyBytes::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap();
|
||||
let pk2 = PublicKeyBytes::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap();
|
||||
let pk3 = PublicKeyBytes::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap();
|
||||
|
||||
// Read once
|
||||
gf.read_graffiti_file().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
gf.load_graffiti(&pk1).unwrap().unwrap(),
|
||||
GraffitiString::from_str(CUSTOM_GRAFFITI1).unwrap().into()
|
||||
);
|
||||
assert_eq!(
|
||||
gf.load_graffiti(&pk2).unwrap().unwrap(),
|
||||
GraffitiString::from_str(CUSTOM_GRAFFITI2).unwrap().into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
gf.load_graffiti(&pk3).unwrap().unwrap(),
|
||||
GraffitiString::from_str(EMPTY_GRAFFITI).unwrap().into()
|
||||
);
|
||||
|
||||
// Random pk should return the default graffiti
|
||||
let random_pk = Keypair::random().pk.compress();
|
||||
assert_eq!(
|
||||
gf.load_graffiti(&random_pk).unwrap().unwrap(),
|
||||
GraffitiString::from_str(DEFAULT_GRAFFITI).unwrap().into()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
use filesystem::create_with_600_perms;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use warp::Filter;
|
||||
|
||||
/// The name of the file which stores the API token.
|
||||
pub const PK_FILENAME: &str = "api-token.txt";
|
||||
|
||||
pub const PK_LEN: usize = 33;
|
||||
|
||||
/// Contains a randomly generated string which is used for authorization of requests to the HTTP API.
|
||||
///
|
||||
/// Provides convenience functions to ultimately provide:
|
||||
///
|
||||
/// - Verification of proof-of-knowledge of the public key in `self` for incoming HTTP requests,
|
||||
/// via the `Authorization` header.
|
||||
///
|
||||
/// The aforementioned scheme was first defined here:
|
||||
///
|
||||
/// https://github.com/sigp/lighthouse/issues/1269#issuecomment-649879855
|
||||
///
|
||||
/// This scheme has since been tweaked to remove VC response signing and secp256k1 key generation.
|
||||
/// https://github.com/sigp/lighthouse/issues/5423
|
||||
pub struct ApiSecret {
|
||||
pk: String,
|
||||
pk_path: PathBuf,
|
||||
}
|
||||
|
||||
impl ApiSecret {
|
||||
/// If the public key is already on-disk, use it.
|
||||
///
|
||||
/// The provided `dir` is a directory containing `PK_FILENAME`.
|
||||
///
|
||||
/// If the public key file is missing on disk, create a new key and
|
||||
/// write it to disk (over-writing any existing files).
|
||||
pub fn create_or_open<P: AsRef<Path>>(dir: P) -> Result<Self, String> {
|
||||
let pk_path = dir.as_ref().join(PK_FILENAME);
|
||||
|
||||
if !pk_path.exists() {
|
||||
let length = PK_LEN;
|
||||
let pk: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(length)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
// Create and write the public key to file with appropriate permissions
|
||||
create_with_600_perms(&pk_path, pk.to_string().as_bytes()).map_err(|e| {
|
||||
format!(
|
||||
"Unable to create file with permissions for {:?}: {:?}",
|
||||
pk_path, e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
let pk = fs::read(&pk_path)
|
||||
.map_err(|e| format!("cannot read {}: {}", PK_FILENAME, e))?
|
||||
.iter()
|
||||
.map(|&c| char::from(c))
|
||||
.collect();
|
||||
|
||||
Ok(Self { pk, pk_path })
|
||||
}
|
||||
|
||||
/// Returns the API token.
|
||||
pub fn api_token(&self) -> String {
|
||||
self.pk.clone()
|
||||
}
|
||||
|
||||
/// Returns the path for the API token file
|
||||
pub fn api_token_path(&self) -> PathBuf {
|
||||
self.pk_path.clone()
|
||||
}
|
||||
|
||||
/// Returns the values of the `Authorization` header which indicate a valid incoming HTTP
|
||||
/// request.
|
||||
///
|
||||
/// For backwards-compatibility we accept the token in a basic authentication style, but this is
|
||||
/// technically invalid according to RFC 7617 because the token is not a base64-encoded username
|
||||
/// and password. As such, bearer authentication should be preferred.
|
||||
fn auth_header_values(&self) -> Vec<String> {
|
||||
vec![
|
||||
format!("Basic {}", self.api_token()),
|
||||
format!("Bearer {}", self.api_token()),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns a `warp` header which filters out request that have a missing or inaccurate
|
||||
/// `Authorization` header.
|
||||
pub fn authorization_header_filter(&self) -> warp::filters::BoxedFilter<()> {
|
||||
let expected = self.auth_header_values();
|
||||
warp::any()
|
||||
.map(move || expected.clone())
|
||||
.and(warp::filters::header::header("Authorization"))
|
||||
.and_then(move |expected: Vec<String>, header: String| async move {
|
||||
if expected.contains(&header) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(warp_utils::reject::invalid_auth(header))
|
||||
}
|
||||
})
|
||||
.untuple_one()
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
use crate::validator_store::ValidatorStore;
|
||||
use bls::{PublicKey, PublicKeyBytes};
|
||||
use eth2::types::GenericResponse;
|
||||
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<GenericResponse<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);
|
||||
if !validator_store.has_validator(&pubkey_bytes) {
|
||||
return Err(warp_utils::reject::custom_not_found(format!(
|
||||
"{} is disabled or not managed by this validator client",
|
||||
pubkey_bytes.as_hex_string()
|
||||
)));
|
||||
}
|
||||
|
||||
let validator_index = validator_store
|
||||
.validator_index(&pubkey_bytes)
|
||||
.ok_or_else(|| {
|
||||
warp_utils::reject::custom_not_found(format!(
|
||||
"The validator index for {} is not known. The validator client \
|
||||
may still be initializing or the validator has not yet had a \
|
||||
deposit processed.",
|
||||
pubkey_bytes.as_hex_string()
|
||||
))
|
||||
})?;
|
||||
|
||||
let voluntary_exit = VoluntaryExit {
|
||||
epoch,
|
||||
validator_index,
|
||||
};
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Signing voluntary exit";
|
||||
"validator" => pubkey_bytes.as_hex_string(),
|
||||
"epoch" => epoch
|
||||
);
|
||||
|
||||
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(GenericResponse::from(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()))
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
use crate::ValidatorStore;
|
||||
use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition};
|
||||
use account_utils::{
|
||||
eth2_keystore::Keystore,
|
||||
eth2_wallet::{bip39::Mnemonic, WalletBuilder},
|
||||
random_mnemonic, random_password, ZeroizeString,
|
||||
};
|
||||
use eth2::lighthouse_vc::types::{self as api_types};
|
||||
use slot_clock::SlotClock;
|
||||
use std::path::{Path, PathBuf};
|
||||
use types::ChainSpec;
|
||||
use types::EthSpec;
|
||||
use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder};
|
||||
|
||||
/// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in
|
||||
/// this validator client.
|
||||
///
|
||||
/// Returns the list of created validators and the mnemonic used to derive them via EIP-2334.
|
||||
///
|
||||
/// ## Detail
|
||||
///
|
||||
/// If `mnemonic_opt` is not supplied it will be randomly generated and returned in the response.
|
||||
///
|
||||
/// If `key_derivation_path_offset` is supplied then the EIP-2334 validator index will start at
|
||||
/// this point.
|
||||
pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock, E: EthSpec>(
|
||||
mnemonic_opt: Option<Mnemonic>,
|
||||
key_derivation_path_offset: Option<u32>,
|
||||
validator_requests: &[api_types::ValidatorRequest],
|
||||
validator_dir: P,
|
||||
secrets_dir: Option<PathBuf>,
|
||||
validator_store: &ValidatorStore<T, E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(Vec<api_types::CreatedValidator>, Mnemonic), warp::Rejection> {
|
||||
let mnemonic = mnemonic_opt.unwrap_or_else(random_mnemonic);
|
||||
|
||||
let wallet_password = random_password();
|
||||
let mut wallet =
|
||||
WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_bytes(), String::new())
|
||||
.and_then(|builder| builder.build())
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"unable to create EIP-2386 wallet: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
if let Some(nextaccount) = key_derivation_path_offset {
|
||||
wallet.set_nextaccount(nextaccount).map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"unable to set wallet nextaccount: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut validators = Vec::with_capacity(validator_requests.len());
|
||||
|
||||
for request in validator_requests {
|
||||
let voting_password = random_password();
|
||||
let withdrawal_password = random_password();
|
||||
let voting_password_string = ZeroizeString::from(
|
||||
String::from_utf8(voting_password.as_bytes().to_vec()).map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"locally generated password is not utf8: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
);
|
||||
|
||||
let mut keystores = wallet
|
||||
.next_validator(
|
||||
wallet_password.as_bytes(),
|
||||
voting_password.as_bytes(),
|
||||
withdrawal_password.as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"unable to create validator keys: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
keystores
|
||||
.voting
|
||||
.set_description(request.description.clone());
|
||||
keystores
|
||||
.withdrawal
|
||||
.set_description(request.description.clone());
|
||||
|
||||
let voting_pubkey = format!("0x{}", keystores.voting.pubkey())
|
||||
.parse()
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"created invalid public key: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let voting_password_storage =
|
||||
get_voting_password_storage(&secrets_dir, &keystores.voting, &voting_password_string)?;
|
||||
|
||||
let validator_dir = ValidatorDirBuilder::new(validator_dir.as_ref().into())
|
||||
.password_dir_opt(secrets_dir.clone())
|
||||
.voting_keystore(keystores.voting, voting_password.as_bytes())
|
||||
.withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes())
|
||||
.create_eth1_tx_data(request.deposit_gwei, spec)
|
||||
.store_withdrawal_keystore(false)
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"failed to build validator directory: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let eth1_deposit_data = validator_dir
|
||||
.eth1_deposit_data()
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"failed to read local deposit data: {:?}",
|
||||
e
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
warp_utils::reject::custom_server_error(
|
||||
"failed to create local deposit data: {:?}".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if eth1_deposit_data.deposit_data.amount != request.deposit_gwei {
|
||||
return Err(warp_utils::reject::custom_server_error(format!(
|
||||
"invalid deposit_gwei {}, expected {}",
|
||||
eth1_deposit_data.deposit_data.amount, request.deposit_gwei
|
||||
)));
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
validator_store
|
||||
.add_validator_keystore(
|
||||
voting_keystore_path,
|
||||
voting_password_storage,
|
||||
request.enable,
|
||||
request.graffiti.clone(),
|
||||
request.suggested_fee_recipient,
|
||||
request.gas_limit,
|
||||
request.builder_proposals,
|
||||
request.builder_boost_factor,
|
||||
request.prefer_builder_proposals,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"failed to initialize validator: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
validators.push(api_types::CreatedValidator {
|
||||
enabled: request.enable,
|
||||
description: request.description.clone(),
|
||||
graffiti: request.graffiti.clone(),
|
||||
suggested_fee_recipient: request.suggested_fee_recipient,
|
||||
gas_limit: request.gas_limit,
|
||||
builder_proposals: request.builder_proposals,
|
||||
voting_pubkey,
|
||||
eth1_deposit_tx_data: serde_utils::hex::encode(ð1_deposit_data.rlp),
|
||||
deposit_gwei: request.deposit_gwei,
|
||||
});
|
||||
}
|
||||
|
||||
Ok((validators, mnemonic))
|
||||
}
|
||||
|
||||
pub async fn create_validators_web3signer<T: 'static + SlotClock, E: EthSpec>(
|
||||
validators: Vec<ValidatorDefinition>,
|
||||
validator_store: &ValidatorStore<T, E>,
|
||||
) -> Result<(), warp::Rejection> {
|
||||
for validator in validators {
|
||||
validator_store
|
||||
.add_validator(validator)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"failed to initialize validator: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to return a `PasswordStorage::File` if `secrets_dir` is defined.
|
||||
/// Otherwise, returns a `PasswordStorage::ValidatorDefinitions`.
|
||||
pub fn get_voting_password_storage(
|
||||
secrets_dir: &Option<PathBuf>,
|
||||
voting_keystore: &Keystore,
|
||||
voting_password_string: &ZeroizeString,
|
||||
) -> Result<PasswordStorage, warp::Rejection> {
|
||||
if let Some(secrets_dir) = &secrets_dir {
|
||||
let password_path = keystore_password_path(secrets_dir, voting_keystore);
|
||||
if password_path.exists() {
|
||||
Err(warp_utils::reject::custom_server_error(
|
||||
"Duplicate keystore password path".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(PasswordStorage::File(password_path))
|
||||
}
|
||||
} else {
|
||||
Ok(PasswordStorage::ValidatorDefinitions(
|
||||
voting_password_string.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
use crate::validator_store::ValidatorStore;
|
||||
use bls::PublicKey;
|
||||
use slot_clock::SlotClock;
|
||||
use std::sync::Arc;
|
||||
use types::{graffiti::GraffitiString, EthSpec, Graffiti};
|
||||
|
||||
pub fn get_graffiti<T: 'static + SlotClock + Clone, E: EthSpec>(
|
||||
validator_pubkey: PublicKey,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
graffiti_flag: Option<Graffiti>,
|
||||
) -> Result<Graffiti, warp::Rejection> {
|
||||
let initialized_validators_rw_lock = validator_store.initialized_validators();
|
||||
let initialized_validators = initialized_validators_rw_lock.read();
|
||||
match initialized_validators.validator(&validator_pubkey.compress()) {
|
||||
None => Err(warp_utils::reject::custom_not_found(
|
||||
"The key was not found on the server".to_string(),
|
||||
)),
|
||||
Some(_) => {
|
||||
let Some(graffiti) = initialized_validators.graffiti(&validator_pubkey.into()) else {
|
||||
return graffiti_flag.ok_or(warp_utils::reject::custom_server_error(
|
||||
"No graffiti found, unable to return the process-wide default".to_string(),
|
||||
));
|
||||
};
|
||||
Ok(graffiti)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_graffiti<T: 'static + SlotClock + Clone, E: EthSpec>(
|
||||
validator_pubkey: PublicKey,
|
||||
graffiti: GraffitiString,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
) -> Result<(), warp::Rejection> {
|
||||
let initialized_validators_rw_lock = validator_store.initialized_validators();
|
||||
let mut initialized_validators = initialized_validators_rw_lock.write();
|
||||
match initialized_validators.validator(&validator_pubkey.compress()) {
|
||||
None => Err(warp_utils::reject::custom_not_found(
|
||||
"The key was not found on the server, nothing to update".to_string(),
|
||||
)),
|
||||
Some(initialized_validator) => {
|
||||
if initialized_validator.get_graffiti() == Some(graffiti.clone().into()) {
|
||||
Ok(())
|
||||
} else {
|
||||
initialized_validators
|
||||
.set_graffiti(&validator_pubkey, graffiti)
|
||||
.map_err(|_| {
|
||||
warp_utils::reject::custom_server_error(
|
||||
"A graffiti was found, but failed to be updated.".to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_graffiti<T: 'static + SlotClock + Clone, E: EthSpec>(
|
||||
validator_pubkey: PublicKey,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
) -> Result<(), warp::Rejection> {
|
||||
let initialized_validators_rw_lock = validator_store.initialized_validators();
|
||||
let mut initialized_validators = initialized_validators_rw_lock.write();
|
||||
match initialized_validators.validator(&validator_pubkey.compress()) {
|
||||
None => Err(warp_utils::reject::custom_not_found(
|
||||
"The key was not found on the server, nothing to delete".to_string(),
|
||||
)),
|
||||
Some(initialized_validator) => {
|
||||
if initialized_validator.get_graffiti().is_none() {
|
||||
Ok(())
|
||||
} else {
|
||||
initialized_validators
|
||||
.delete_graffiti(&validator_pubkey)
|
||||
.map_err(|_| {
|
||||
warp_utils::reject::custom_server_error(
|
||||
"A graffiti was found, but failed to be removed.".to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
//! Implementation of the standard keystore management API.
|
||||
use crate::{
|
||||
initialized_validators::Error, signing_method::SigningMethod, InitializedValidators,
|
||||
ValidatorStore,
|
||||
};
|
||||
use account_utils::{validator_definitions::PasswordStorage, ZeroizeString};
|
||||
use eth2::lighthouse_vc::{
|
||||
std_types::{
|
||||
DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse,
|
||||
ImportKeystoreStatus, ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr,
|
||||
KeystoreJsonStr, ListKeystoresResponse, SingleKeystoreResponse, Status,
|
||||
},
|
||||
types::{ExportKeystoresResponse, SingleExportKeystoresResponse},
|
||||
};
|
||||
use eth2_keystore::Keystore;
|
||||
use slog::{info, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use task_executor::TaskExecutor;
|
||||
use tokio::runtime::Handle;
|
||||
use types::{EthSpec, PublicKeyBytes};
|
||||
use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder};
|
||||
use warp::Rejection;
|
||||
use warp_utils::reject::{custom_bad_request, custom_server_error};
|
||||
|
||||
pub fn list<T: SlotClock + 'static, E: EthSpec>(
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
) -> ListKeystoresResponse {
|
||||
let initialized_validators_rwlock = validator_store.initialized_validators();
|
||||
let initialized_validators = initialized_validators_rwlock.read();
|
||||
|
||||
let keystores = initialized_validators
|
||||
.validator_definitions()
|
||||
.iter()
|
||||
.filter(|def| def.enabled)
|
||||
.map(|def| {
|
||||
let validating_pubkey = def.voting_public_key.compress();
|
||||
|
||||
let (derivation_path, readonly) = initialized_validators
|
||||
.signing_method(&validating_pubkey)
|
||||
.map_or((None, None), |signing_method| match *signing_method {
|
||||
SigningMethod::LocalKeystore {
|
||||
ref voting_keystore,
|
||||
..
|
||||
} => (voting_keystore.path(), Some(false)),
|
||||
SigningMethod::Web3Signer { .. } => (None, Some(true)),
|
||||
});
|
||||
|
||||
SingleKeystoreResponse {
|
||||
validating_pubkey,
|
||||
derivation_path,
|
||||
readonly,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ListKeystoresResponse { data: keystores }
|
||||
}
|
||||
|
||||
pub fn import<T: SlotClock + 'static, E: EthSpec>(
|
||||
request: ImportKeystoresRequest,
|
||||
validator_dir: PathBuf,
|
||||
secrets_dir: Option<PathBuf>,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
task_executor: TaskExecutor,
|
||||
log: Logger,
|
||||
) -> Result<ImportKeystoresResponse, Rejection> {
|
||||
// Check request validity. This is the only cases in which we should return a 4xx code.
|
||||
if request.keystores.len() != request.passwords.len() {
|
||||
return Err(custom_bad_request(format!(
|
||||
"mismatched numbers of keystores ({}) and passwords ({})",
|
||||
request.keystores.len(),
|
||||
request.passwords.len(),
|
||||
)));
|
||||
}
|
||||
|
||||
// Import slashing protection data before keystores, so that new keystores don't start signing
|
||||
// without it. Do not return early on failure, propagate the failure to each key.
|
||||
let slashing_protection_status =
|
||||
if let Some(InterchangeJsonStr(slashing_protection)) = request.slashing_protection {
|
||||
// Warn for missing slashing protection.
|
||||
for KeystoreJsonStr(ref keystore) in &request.keystores {
|
||||
if let Some(public_key) = keystore.public_key() {
|
||||
let pubkey_bytes = public_key.compress();
|
||||
if !slashing_protection
|
||||
.data
|
||||
.iter()
|
||||
.any(|data| data.pubkey == pubkey_bytes)
|
||||
{
|
||||
warn!(
|
||||
log,
|
||||
"Slashing protection data not provided";
|
||||
"public_key" => ?public_key,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator_store.import_slashing_protection(slashing_protection)
|
||||
} else {
|
||||
warn!(log, "No slashing protection data provided with keystores");
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// Import each keystore. Some keystores may fail to be imported, so we record a status for each.
|
||||
let mut statuses = Vec::with_capacity(request.keystores.len());
|
||||
|
||||
for (KeystoreJsonStr(keystore), password) in request
|
||||
.keystores
|
||||
.into_iter()
|
||||
.zip(request.passwords.into_iter())
|
||||
{
|
||||
let pubkey_str = keystore.pubkey().to_string();
|
||||
|
||||
let status = if let Err(e) = &slashing_protection_status {
|
||||
// Slashing protection import failed, do not attempt to import the key. Record an
|
||||
// error status.
|
||||
Status::error(
|
||||
ImportKeystoreStatus::Error,
|
||||
format!("slashing protection import failed: {:?}", e),
|
||||
)
|
||||
} else if let Some(handle) = task_executor.handle() {
|
||||
// Import the keystore.
|
||||
match import_single_keystore(
|
||||
keystore,
|
||||
password,
|
||||
validator_dir.clone(),
|
||||
secrets_dir.clone(),
|
||||
&validator_store,
|
||||
handle,
|
||||
) {
|
||||
Ok(status) => Status::ok(status),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
log,
|
||||
"Error importing keystore, skipped";
|
||||
"pubkey" => pubkey_str,
|
||||
"error" => ?e,
|
||||
);
|
||||
Status::error(ImportKeystoreStatus::Error, e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Status::error(
|
||||
ImportKeystoreStatus::Error,
|
||||
"validator client shutdown".into(),
|
||||
)
|
||||
};
|
||||
statuses.push(status);
|
||||
}
|
||||
|
||||
let successful_import = statuses
|
||||
.iter()
|
||||
.filter(|status| matches!(status.status, ImportKeystoreStatus::Imported))
|
||||
.count();
|
||||
|
||||
if successful_import > 0 {
|
||||
info!(
|
||||
log,
|
||||
"Imported keystores via standard HTTP API";
|
||||
"count" => successful_import,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(ImportKeystoresResponse { data: statuses })
|
||||
}
|
||||
|
||||
fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>(
|
||||
keystore: Keystore,
|
||||
password: ZeroizeString,
|
||||
validator_dir_path: PathBuf,
|
||||
secrets_dir: Option<PathBuf>,
|
||||
validator_store: &ValidatorStore<T, E>,
|
||||
handle: Handle,
|
||||
) -> Result<ImportKeystoreStatus, String> {
|
||||
// Check if the validator key already exists, erroring if it is a remote signer validator.
|
||||
let pubkey = keystore
|
||||
.public_key()
|
||||
.ok_or_else(|| format!("invalid pubkey: {}", keystore.pubkey()))?;
|
||||
if let Some(def) = validator_store
|
||||
.initialized_validators()
|
||||
.read()
|
||||
.validator_definitions()
|
||||
.iter()
|
||||
.find(|def| def.voting_public_key == pubkey)
|
||||
{
|
||||
if !def.signing_definition.is_local_keystore() {
|
||||
return Err("cannot import duplicate of existing remote signer validator".into());
|
||||
} else if def.enabled {
|
||||
return Ok(ImportKeystoreStatus::Duplicate);
|
||||
}
|
||||
}
|
||||
|
||||
let password_storage = if let Some(secrets_dir) = &secrets_dir {
|
||||
let password_path = keystore_password_path(secrets_dir, &keystore);
|
||||
if password_path.exists() {
|
||||
return Ok(ImportKeystoreStatus::Duplicate);
|
||||
}
|
||||
PasswordStorage::File(password_path)
|
||||
} else {
|
||||
PasswordStorage::ValidatorDefinitions(password.clone())
|
||||
};
|
||||
|
||||
// Check that the password is correct.
|
||||
// In future we should re-structure to avoid the double decryption here. It's not as simple
|
||||
// as removing this check because `add_validator_keystore` will break if provided with an
|
||||
// invalid validator definition (`update_validators` will get stuck trying to decrypt with the
|
||||
// wrong password indefinitely).
|
||||
keystore
|
||||
.decrypt_keypair(password.as_ref())
|
||||
.map_err(|e| format!("incorrect password: {:?}", e))?;
|
||||
|
||||
let validator_dir = ValidatorDirBuilder::new(validator_dir_path)
|
||||
.password_dir_opt(secrets_dir)
|
||||
.voting_keystore(keystore, password.as_ref())
|
||||
.store_withdrawal_keystore(false)
|
||||
.build()
|
||||
.map_err(|e| 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);
|
||||
|
||||
handle
|
||||
.block_on(validator_store.add_validator_keystore(
|
||||
voting_keystore_path,
|
||||
password_storage,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
))
|
||||
.map_err(|e| format!("failed to initialize validator: {:?}", e))?;
|
||||
|
||||
Ok(ImportKeystoreStatus::Imported)
|
||||
}
|
||||
|
||||
pub fn delete<T: SlotClock + 'static, E: EthSpec>(
|
||||
request: DeleteKeystoresRequest,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
task_executor: TaskExecutor,
|
||||
log: Logger,
|
||||
) -> Result<DeleteKeystoresResponse, Rejection> {
|
||||
let export_response = export(request, validator_store, task_executor, log.clone())?;
|
||||
|
||||
// Check the status is Deleted to confirm deletion is successful, then only display the log
|
||||
let successful_deletion = export_response
|
||||
.data
|
||||
.iter()
|
||||
.filter(|response| matches!(response.status.status, DeleteKeystoreStatus::Deleted))
|
||||
.count();
|
||||
|
||||
if successful_deletion > 0 {
|
||||
info!(
|
||||
log,
|
||||
"Deleted keystore via standard HTTP API";
|
||||
"count" => successful_deletion,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(DeleteKeystoresResponse {
|
||||
data: export_response
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|response| response.status)
|
||||
.collect(),
|
||||
slashing_protection: export_response.slashing_protection,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn export<T: SlotClock + 'static, E: EthSpec>(
|
||||
request: DeleteKeystoresRequest,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
task_executor: TaskExecutor,
|
||||
log: Logger,
|
||||
) -> Result<ExportKeystoresResponse, Rejection> {
|
||||
// Remove from initialized validators.
|
||||
let initialized_validators_rwlock = validator_store.initialized_validators();
|
||||
let mut initialized_validators = initialized_validators_rwlock.write();
|
||||
|
||||
let mut responses = request
|
||||
.pubkeys
|
||||
.iter()
|
||||
.map(|pubkey_bytes| {
|
||||
match delete_single_keystore(
|
||||
pubkey_bytes,
|
||||
&mut initialized_validators,
|
||||
task_executor.clone(),
|
||||
) {
|
||||
Ok(status) => status,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
log,
|
||||
"Error deleting keystore";
|
||||
"pubkey" => ?pubkey_bytes,
|
||||
"error" => ?error,
|
||||
);
|
||||
SingleExportKeystoresResponse {
|
||||
status: Status::error(DeleteKeystoreStatus::Error, error),
|
||||
validating_keystore: None,
|
||||
validating_keystore_password: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Use `update_validators` to update the key cache. It is safe to let the key cache get a bit out
|
||||
// of date as it resets when it can't be decrypted. We update it just a single time to avoid
|
||||
// continually resetting it after each key deletion.
|
||||
if let Some(handle) = task_executor.handle() {
|
||||
handle
|
||||
.block_on(initialized_validators.update_validators())
|
||||
.map_err(|e| custom_server_error(format!("unable to update key cache: {:?}", e)))?;
|
||||
}
|
||||
|
||||
// Export the slashing protection data.
|
||||
let slashing_protection = validator_store
|
||||
.export_slashing_protection_for_keys(&request.pubkeys)
|
||||
.map_err(|e| {
|
||||
custom_server_error(format!("error exporting slashing protection: {:?}", e))
|
||||
})?;
|
||||
|
||||
// Update stasuses based on availability of slashing protection data.
|
||||
for (pubkey, response) in request.pubkeys.iter().zip(responses.iter_mut()) {
|
||||
if response.status.status == DeleteKeystoreStatus::NotFound
|
||||
&& slashing_protection
|
||||
.data
|
||||
.iter()
|
||||
.any(|interchange_data| interchange_data.pubkey == *pubkey)
|
||||
{
|
||||
response.status.status = DeleteKeystoreStatus::NotActive;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExportKeystoresResponse {
|
||||
data: responses,
|
||||
slashing_protection,
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_single_keystore(
|
||||
pubkey_bytes: &PublicKeyBytes,
|
||||
initialized_validators: &mut InitializedValidators,
|
||||
task_executor: TaskExecutor,
|
||||
) -> Result<SingleExportKeystoresResponse, String> {
|
||||
if let Some(handle) = task_executor.handle() {
|
||||
let pubkey = pubkey_bytes
|
||||
.decompress()
|
||||
.map_err(|e| format!("invalid pubkey, {:?}: {:?}", pubkey_bytes, e))?;
|
||||
|
||||
match handle.block_on(initialized_validators.delete_definition_and_keystore(&pubkey, true))
|
||||
{
|
||||
Ok(Some(keystore_and_password)) => Ok(SingleExportKeystoresResponse {
|
||||
status: Status::ok(DeleteKeystoreStatus::Deleted),
|
||||
validating_keystore: Some(KeystoreJsonStr(keystore_and_password.keystore)),
|
||||
validating_keystore_password: keystore_and_password.password,
|
||||
}),
|
||||
Ok(None) => Ok(SingleExportKeystoresResponse {
|
||||
status: Status::ok(DeleteKeystoreStatus::Deleted),
|
||||
validating_keystore: None,
|
||||
validating_keystore_password: None,
|
||||
}),
|
||||
Err(e) => match e {
|
||||
Error::ValidatorNotInitialized(_) => Ok(SingleExportKeystoresResponse {
|
||||
status: Status::ok(DeleteKeystoreStatus::NotFound),
|
||||
validating_keystore: None,
|
||||
validating_keystore_password: None,
|
||||
}),
|
||||
_ => Err(format!("unable to disable and delete: {:?}", e)),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Err("validator client shutdown".into())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,217 +0,0 @@
|
||||
//! Implementation of the standard remotekey management API.
|
||||
use crate::{initialized_validators::Error, InitializedValidators, ValidatorStore};
|
||||
use account_utils::validator_definitions::{
|
||||
SigningDefinition, ValidatorDefinition, Web3SignerDefinition,
|
||||
};
|
||||
use eth2::lighthouse_vc::std_types::{
|
||||
DeleteRemotekeyStatus, DeleteRemotekeysRequest, DeleteRemotekeysResponse,
|
||||
ImportRemotekeyStatus, ImportRemotekeysRequest, ImportRemotekeysResponse,
|
||||
ListRemotekeysResponse, SingleListRemotekeysResponse, Status,
|
||||
};
|
||||
use slog::{info, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use std::sync::Arc;
|
||||
use task_executor::TaskExecutor;
|
||||
use tokio::runtime::Handle;
|
||||
use types::{EthSpec, PublicKeyBytes};
|
||||
use url::Url;
|
||||
use warp::Rejection;
|
||||
use warp_utils::reject::custom_server_error;
|
||||
|
||||
pub fn list<T: SlotClock + 'static, E: EthSpec>(
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
) -> ListRemotekeysResponse {
|
||||
let initialized_validators_rwlock = validator_store.initialized_validators();
|
||||
let initialized_validators = initialized_validators_rwlock.read();
|
||||
|
||||
let keystores = initialized_validators
|
||||
.validator_definitions()
|
||||
.iter()
|
||||
.filter(|def| def.enabled)
|
||||
.filter_map(|def| {
|
||||
let validating_pubkey = def.voting_public_key.compress();
|
||||
|
||||
match &def.signing_definition {
|
||||
SigningDefinition::LocalKeystore { .. } => None,
|
||||
SigningDefinition::Web3Signer(Web3SignerDefinition { url, .. }) => {
|
||||
Some(SingleListRemotekeysResponse {
|
||||
pubkey: validating_pubkey,
|
||||
url: url.clone(),
|
||||
readonly: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ListRemotekeysResponse { data: keystores }
|
||||
}
|
||||
|
||||
pub fn import<T: SlotClock + 'static, E: EthSpec>(
|
||||
request: ImportRemotekeysRequest,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
task_executor: TaskExecutor,
|
||||
log: Logger,
|
||||
) -> Result<ImportRemotekeysResponse, Rejection> {
|
||||
info!(
|
||||
log,
|
||||
"Importing remotekeys via standard HTTP API";
|
||||
"count" => request.remote_keys.len(),
|
||||
);
|
||||
// Import each remotekey. Some remotekeys may fail to be imported, so we record a status for each.
|
||||
let mut statuses = Vec::with_capacity(request.remote_keys.len());
|
||||
|
||||
for remotekey in request.remote_keys {
|
||||
let status = if let Some(handle) = task_executor.handle() {
|
||||
// Import the keystore.
|
||||
match import_single_remotekey(remotekey.pubkey, remotekey.url, &validator_store, handle)
|
||||
{
|
||||
Ok(status) => Status::ok(status),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
log,
|
||||
"Error importing keystore, skipped";
|
||||
"pubkey" => remotekey.pubkey.to_string(),
|
||||
"error" => ?e,
|
||||
);
|
||||
Status::error(ImportRemotekeyStatus::Error, e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Status::error(
|
||||
ImportRemotekeyStatus::Error,
|
||||
"validator client shutdown".into(),
|
||||
)
|
||||
};
|
||||
statuses.push(status);
|
||||
}
|
||||
Ok(ImportRemotekeysResponse { data: statuses })
|
||||
}
|
||||
|
||||
fn import_single_remotekey<T: SlotClock + 'static, E: EthSpec>(
|
||||
pubkey: PublicKeyBytes,
|
||||
url: String,
|
||||
validator_store: &ValidatorStore<T, E>,
|
||||
handle: Handle,
|
||||
) -> Result<ImportRemotekeyStatus, String> {
|
||||
if let Err(url_err) = Url::parse(&url) {
|
||||
return Err(format!("failed to parse remotekey URL: {}", url_err));
|
||||
}
|
||||
|
||||
let pubkey = pubkey
|
||||
.decompress()
|
||||
.map_err(|_| format!("invalid pubkey: {}", pubkey))?;
|
||||
|
||||
if let Some(def) = validator_store
|
||||
.initialized_validators()
|
||||
.read()
|
||||
.validator_definitions()
|
||||
.iter()
|
||||
.find(|def| def.voting_public_key == pubkey)
|
||||
{
|
||||
if def.signing_definition.is_local_keystore() {
|
||||
return Err("Pubkey already present in local keystore.".into());
|
||||
} else if def.enabled {
|
||||
return Ok(ImportRemotekeyStatus::Duplicate);
|
||||
}
|
||||
}
|
||||
|
||||
// Remotekeys are stored as web3signers.
|
||||
// The remotekey API provides less confgiuration option than the web3signer API.
|
||||
let web3signer_validator = ValidatorDefinition {
|
||||
enabled: true,
|
||||
voting_public_key: pubkey,
|
||||
graffiti: None,
|
||||
suggested_fee_recipient: None,
|
||||
gas_limit: None,
|
||||
builder_proposals: None,
|
||||
builder_boost_factor: None,
|
||||
prefer_builder_proposals: None,
|
||||
description: String::from("Added by remotekey API"),
|
||||
signing_definition: SigningDefinition::Web3Signer(Web3SignerDefinition {
|
||||
url,
|
||||
root_certificate_path: None,
|
||||
request_timeout_ms: None,
|
||||
client_identity_path: None,
|
||||
client_identity_password: None,
|
||||
}),
|
||||
};
|
||||
handle
|
||||
.block_on(validator_store.add_validator(web3signer_validator))
|
||||
.map_err(|e| format!("failed to initialize validator: {:?}", e))?;
|
||||
|
||||
Ok(ImportRemotekeyStatus::Imported)
|
||||
}
|
||||
|
||||
pub fn delete<T: SlotClock + 'static, E: EthSpec>(
|
||||
request: DeleteRemotekeysRequest,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
task_executor: TaskExecutor,
|
||||
log: Logger,
|
||||
) -> Result<DeleteRemotekeysResponse, Rejection> {
|
||||
info!(
|
||||
log,
|
||||
"Deleting remotekeys via standard HTTP API";
|
||||
"count" => request.pubkeys.len(),
|
||||
);
|
||||
// Remove from initialized validators.
|
||||
let initialized_validators_rwlock = validator_store.initialized_validators();
|
||||
let mut initialized_validators = initialized_validators_rwlock.write();
|
||||
|
||||
let statuses = request
|
||||
.pubkeys
|
||||
.iter()
|
||||
.map(|pubkey_bytes| {
|
||||
match delete_single_remotekey(
|
||||
pubkey_bytes,
|
||||
&mut initialized_validators,
|
||||
task_executor.clone(),
|
||||
) {
|
||||
Ok(status) => Status::ok(status),
|
||||
Err(error) => {
|
||||
warn!(
|
||||
log,
|
||||
"Error deleting keystore";
|
||||
"pubkey" => ?pubkey_bytes,
|
||||
"error" => ?error,
|
||||
);
|
||||
Status::error(DeleteRemotekeyStatus::Error, error)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Use `update_validators` to update the key cache. It is safe to let the key cache get a bit out
|
||||
// of date as it resets when it can't be decrypted. We update it just a single time to avoid
|
||||
// continually resetting it after each key deletion.
|
||||
if let Some(handle) = task_executor.handle() {
|
||||
handle
|
||||
.block_on(initialized_validators.update_validators())
|
||||
.map_err(|e| custom_server_error(format!("unable to update key cache: {:?}", e)))?;
|
||||
}
|
||||
|
||||
Ok(DeleteRemotekeysResponse { data: statuses })
|
||||
}
|
||||
|
||||
fn delete_single_remotekey(
|
||||
pubkey_bytes: &PublicKeyBytes,
|
||||
initialized_validators: &mut InitializedValidators,
|
||||
task_executor: TaskExecutor,
|
||||
) -> Result<DeleteRemotekeyStatus, String> {
|
||||
if let Some(handle) = task_executor.handle() {
|
||||
let pubkey = pubkey_bytes
|
||||
.decompress()
|
||||
.map_err(|e| format!("invalid pubkey, {:?}: {:?}", pubkey_bytes, e))?;
|
||||
|
||||
match handle.block_on(initialized_validators.delete_definition_and_keystore(&pubkey, false))
|
||||
{
|
||||
Ok(_) => Ok(DeleteRemotekeyStatus::Deleted),
|
||||
Err(e) => match e {
|
||||
Error::ValidatorNotInitialized(_) => Ok(DeleteRemotekeyStatus::NotFound),
|
||||
_ => Err(format!("unable to disable and delete: {:?}", e)),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Err("validator client shutdown".into())
|
||||
}
|
||||
}
|
||||
@@ -1,653 +0,0 @@
|
||||
use crate::doppelganger_service::DoppelgangerService;
|
||||
use crate::key_cache::{KeyCache, CACHE_FILENAME};
|
||||
use crate::{
|
||||
http_api::{ApiSecret, Config as HttpConfig, Context},
|
||||
initialized_validators::{InitializedValidators, OnDecryptFailure},
|
||||
Config, ValidatorDefinitions, ValidatorStore,
|
||||
};
|
||||
use account_utils::{
|
||||
eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password,
|
||||
ZeroizeString,
|
||||
};
|
||||
use deposit_contract::decode_eth1_tx_data;
|
||||
use eth2::{
|
||||
lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*},
|
||||
types::ErrorMessage as ApiErrorMessage,
|
||||
Error as ApiError,
|
||||
};
|
||||
use eth2_keystore::KeystoreBuilder;
|
||||
use logging::test_logger;
|
||||
use parking_lot::RwLock;
|
||||
use sensitive_url::SensitiveUrl;
|
||||
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
|
||||
use slot_clock::{SlotClock, TestingSlotClock};
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use task_executor::test_utils::TestRuntime;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37];
|
||||
pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42);
|
||||
|
||||
type E = MainnetEthSpec;
|
||||
|
||||
pub struct HdValidatorScenario {
|
||||
pub count: usize,
|
||||
pub specify_mnemonic: bool,
|
||||
pub key_derivation_path_offset: u32,
|
||||
pub disabled: Vec<usize>,
|
||||
}
|
||||
|
||||
pub struct KeystoreValidatorScenario {
|
||||
pub enabled: bool,
|
||||
pub correct_password: bool,
|
||||
}
|
||||
|
||||
pub struct Web3SignerValidatorScenario {
|
||||
pub count: usize,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub struct ApiTester {
|
||||
pub client: ValidatorClientHttpClient,
|
||||
pub initialized_validators: Arc<RwLock<InitializedValidators>>,
|
||||
pub validator_store: Arc<ValidatorStore<TestingSlotClock, E>>,
|
||||
pub url: SensitiveUrl,
|
||||
pub api_token: String,
|
||||
pub test_runtime: TestRuntime,
|
||||
pub _server_shutdown: oneshot::Sender<()>,
|
||||
pub validator_dir: TempDir,
|
||||
pub secrets_dir: TempDir,
|
||||
}
|
||||
|
||||
impl ApiTester {
|
||||
pub async fn new() -> Self {
|
||||
Self::new_with_http_config(Self::default_http_config()).await
|
||||
}
|
||||
|
||||
pub async fn new_with_http_config(http_config: HttpConfig) -> Self {
|
||||
let log = test_logger();
|
||||
|
||||
let validator_dir = tempdir().unwrap();
|
||||
let secrets_dir = tempdir().unwrap();
|
||||
|
||||
let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap();
|
||||
|
||||
let initialized_validators = InitializedValidators::from_definitions(
|
||||
validator_defs,
|
||||
validator_dir.path().into(),
|
||||
Default::default(),
|
||||
log.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap();
|
||||
let api_pubkey = api_secret.api_token();
|
||||
|
||||
let config = Config {
|
||||
validator_dir: validator_dir.path().into(),
|
||||
secrets_dir: secrets_dir.path().into(),
|
||||
fee_recipient: Some(TEST_DEFAULT_FEE_RECIPIENT),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = Arc::new(E::default_spec());
|
||||
|
||||
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 test_runtime = TestRuntime::default();
|
||||
|
||||
let validator_store = Arc::new(ValidatorStore::<_, E>::new(
|
||||
initialized_validators,
|
||||
slashing_protection,
|
||||
Hash256::repeat_byte(42),
|
||||
spec.clone(),
|
||||
Some(Arc::new(DoppelgangerService::new(log.clone()))),
|
||||
slot_clock.clone(),
|
||||
&config,
|
||||
test_runtime.task_executor.clone(),
|
||||
log.clone(),
|
||||
));
|
||||
|
||||
validator_store
|
||||
.register_all_in_doppelganger_protection_if_enabled()
|
||||
.expect("Should attach doppelganger service");
|
||||
|
||||
let initialized_validators = validator_store.initialized_validators();
|
||||
|
||||
let context = Arc::new(Context {
|
||||
task_executor: test_runtime.task_executor.clone(),
|
||||
api_secret,
|
||||
block_service: None,
|
||||
validator_dir: Some(validator_dir.path().into()),
|
||||
secrets_dir: Some(secrets_dir.path().into()),
|
||||
validator_store: Some(validator_store.clone()),
|
||||
graffiti_file: None,
|
||||
graffiti_flag: Some(Graffiti::default()),
|
||||
spec,
|
||||
config: http_config,
|
||||
log,
|
||||
sse_logging_components: None,
|
||||
slot_clock,
|
||||
_phantom: PhantomData,
|
||||
});
|
||||
let ctx = context;
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let server_shutdown = async {
|
||||
// It's not really interesting why this triggered, just that it happened.
|
||||
let _ = shutdown_rx.await;
|
||||
};
|
||||
let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap();
|
||||
|
||||
tokio::spawn(server);
|
||||
|
||||
let url = SensitiveUrl::parse(&format!(
|
||||
"http://{}:{}",
|
||||
listening_socket.ip(),
|
||||
listening_socket.port()
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let client = ValidatorClientHttpClient::new(url.clone(), api_pubkey.clone()).unwrap();
|
||||
|
||||
Self {
|
||||
client,
|
||||
initialized_validators,
|
||||
validator_store,
|
||||
url,
|
||||
api_token: api_pubkey,
|
||||
test_runtime,
|
||||
_server_shutdown: shutdown_tx,
|
||||
validator_dir,
|
||||
secrets_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_http_config() -> HttpConfig {
|
||||
HttpConfig {
|
||||
enabled: true,
|
||||
listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
listen_port: 0,
|
||||
allow_origin: None,
|
||||
allow_keystore_export: true,
|
||||
store_passwords_in_secrets_dir: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that the key cache exists and can be decrypted with the current
|
||||
/// set of known validators.
|
||||
#[allow(clippy::await_holding_lock)] // This is a test, so it should be fine.
|
||||
pub async fn ensure_key_cache_consistency(&self) {
|
||||
assert!(
|
||||
self.validator_dir.as_ref().join(CACHE_FILENAME).exists(),
|
||||
"the key cache should exist"
|
||||
);
|
||||
let key_cache =
|
||||
KeyCache::open_or_create(self.validator_dir.as_ref()).expect("should open a key cache");
|
||||
|
||||
self.initialized_validators
|
||||
.read()
|
||||
.decrypt_key_cache(key_cache, &mut <_>::default(), OnDecryptFailure::Error)
|
||||
.await
|
||||
.expect("key cache should decypt");
|
||||
}
|
||||
|
||||
pub fn invalid_token_client(&self) -> ValidatorClientHttpClient {
|
||||
let tmp = tempdir().unwrap();
|
||||
let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap();
|
||||
let invalid_pubkey = api_secret.api_token();
|
||||
ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey).unwrap()
|
||||
}
|
||||
|
||||
pub async fn test_with_invalid_auth<F, A, T>(self, func: F) -> Self
|
||||
where
|
||||
F: Fn(ValidatorClientHttpClient) -> A,
|
||||
A: Future<Output = Result<T, ApiError>>,
|
||||
{
|
||||
/*
|
||||
* Test with an invalid Authorization header.
|
||||
*/
|
||||
match func(self.invalid_token_client()).await {
|
||||
Err(ApiError::ServerMessage(ApiErrorMessage { code: 403, .. })) => (),
|
||||
Err(other) => panic!("expected authorized error, got {:?}", other),
|
||||
Ok(_) => panic!("expected authorized error, got Ok"),
|
||||
}
|
||||
|
||||
/*
|
||||
* Test with a missing Authorization header.
|
||||
*/
|
||||
let mut missing_token_client = self.client.clone();
|
||||
missing_token_client.send_authorization_header(false);
|
||||
match func(missing_token_client).await {
|
||||
Err(ApiError::ServerMessage(ApiErrorMessage {
|
||||
code: 401, message, ..
|
||||
})) if message.contains("missing Authorization header") => (),
|
||||
Err(other) => panic!("expected missing header error, got {:?}", other),
|
||||
Ok(_) => panic!("expected missing header error, got Ok"),
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn invalidate_api_token(mut self) -> Self {
|
||||
self.client = self.invalid_token_client();
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_lighthouse_version_invalid(self) -> Self {
|
||||
self.client.get_lighthouse_version().await.unwrap_err();
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_lighthouse_spec(self) -> Self {
|
||||
let result = self
|
||||
.client
|
||||
.get_lighthouse_spec::<ConfigAndPresetElectra>()
|
||||
.await
|
||||
.map(|res| ConfigAndPreset::Electra(res.data))
|
||||
.unwrap();
|
||||
let expected = ConfigAndPreset::from_chain_spec::<E>(&E::default_spec(), None);
|
||||
|
||||
assert_eq!(result, expected);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_lighthouse_version(self) -> Self {
|
||||
let result = self.client.get_lighthouse_version().await.unwrap().data;
|
||||
|
||||
let expected = VersionData {
|
||||
version: lighthouse_version::version_with_platform(),
|
||||
};
|
||||
|
||||
assert_eq!(result, expected);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn test_get_lighthouse_health(self) -> Self {
|
||||
self.client.get_lighthouse_health().await.unwrap();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub async fn test_get_lighthouse_health(self) -> Self {
|
||||
self.client.get_lighthouse_health().await.unwrap_err();
|
||||
|
||||
self
|
||||
}
|
||||
pub fn vals_total(&self) -> usize {
|
||||
self.initialized_validators.read().num_total()
|
||||
}
|
||||
|
||||
pub fn vals_enabled(&self) -> usize {
|
||||
self.initialized_validators.read().num_enabled()
|
||||
}
|
||||
|
||||
pub fn assert_enabled_validators_count(self, count: usize) -> Self {
|
||||
assert_eq!(self.vals_enabled(), count);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assert_validators_count(self, count: usize) -> Self {
|
||||
assert_eq!(self.vals_total(), count);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn create_hd_validators(self, s: HdValidatorScenario) -> Self {
|
||||
let initial_vals = self.vals_total();
|
||||
let initial_enabled_vals = self.vals_enabled();
|
||||
|
||||
let validators = (0..s.count)
|
||||
.map(|i| ValidatorRequest {
|
||||
enable: !s.disabled.contains(&i),
|
||||
description: format!("boi #{}", i),
|
||||
graffiti: None,
|
||||
suggested_fee_recipient: None,
|
||||
gas_limit: None,
|
||||
builder_proposals: None,
|
||||
builder_boost_factor: None,
|
||||
prefer_builder_proposals: None,
|
||||
deposit_gwei: E::default_spec().max_effective_balance,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (response, mnemonic) = if s.specify_mnemonic {
|
||||
let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string());
|
||||
let request = CreateValidatorsMnemonicRequest {
|
||||
mnemonic: mnemonic.clone(),
|
||||
key_derivation_path_offset: s.key_derivation_path_offset,
|
||||
validators: validators.clone(),
|
||||
};
|
||||
let response = self
|
||||
.client
|
||||
.post_lighthouse_validators_mnemonic(&request)
|
||||
.await
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
(response, mnemonic)
|
||||
} else {
|
||||
assert_eq!(
|
||||
s.key_derivation_path_offset, 0,
|
||||
"cannot use a derivation offset without specifying a mnemonic"
|
||||
);
|
||||
let response = self
|
||||
.client
|
||||
.post_lighthouse_validators(validators.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.data;
|
||||
(response.validators.clone(), response.mnemonic)
|
||||
};
|
||||
|
||||
assert_eq!(response.len(), s.count);
|
||||
assert_eq!(self.vals_total(), initial_vals + s.count);
|
||||
assert_eq!(
|
||||
self.vals_enabled(),
|
||||
initial_enabled_vals + s.count - s.disabled.len()
|
||||
);
|
||||
|
||||
let server_vals = self.client.get_lighthouse_validators().await.unwrap().data;
|
||||
|
||||
assert_eq!(server_vals.len(), self.vals_total());
|
||||
|
||||
// Ensure the server lists all of these newly created validators.
|
||||
for validator in &response {
|
||||
assert!(server_vals
|
||||
.iter()
|
||||
.any(|server_val| server_val.voting_pubkey == validator.voting_pubkey));
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify that we can regenerate all the keys from the mnemonic.
|
||||
*/
|
||||
|
||||
let mnemonic = mnemonic_from_phrase(mnemonic.as_str()).unwrap();
|
||||
let mut wallet = WalletBuilder::from_mnemonic(&mnemonic, PASSWORD_BYTES, "".to_string())
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
wallet
|
||||
.set_nextaccount(s.key_derivation_path_offset)
|
||||
.unwrap();
|
||||
|
||||
for item in response.iter().take(s.count) {
|
||||
let keypairs = wallet
|
||||
.next_validator(PASSWORD_BYTES, PASSWORD_BYTES, PASSWORD_BYTES)
|
||||
.unwrap();
|
||||
let voting_keypair = keypairs.voting.decrypt_keypair(PASSWORD_BYTES).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
item.voting_pubkey,
|
||||
voting_keypair.pk.clone().into(),
|
||||
"the locally generated voting pk should match the server response"
|
||||
);
|
||||
|
||||
let withdrawal_keypair = keypairs.withdrawal.decrypt_keypair(PASSWORD_BYTES).unwrap();
|
||||
|
||||
let deposit_bytes = serde_utils::hex::decode(&item.eth1_deposit_tx_data).unwrap();
|
||||
|
||||
let (deposit_data, _) =
|
||||
decode_eth1_tx_data(&deposit_bytes, E::default_spec().max_effective_balance)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
deposit_data.pubkey,
|
||||
voting_keypair.pk.clone().into(),
|
||||
"the locally generated voting pk should match the deposit data"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
deposit_data.withdrawal_credentials,
|
||||
Hash256::from_slice(&bls::get_withdrawal_credentials(
|
||||
&withdrawal_keypair.pk,
|
||||
E::default_spec().bls_withdrawal_prefix_byte
|
||||
)),
|
||||
"the locally generated withdrawal creds should match the deposit data"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
deposit_data.signature,
|
||||
deposit_data.create_signature(&voting_keypair.sk, &E::default_spec()),
|
||||
"the locally-generated deposit sig should create the same deposit sig"
|
||||
);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn create_keystore_validators(self, s: KeystoreValidatorScenario) -> Self {
|
||||
let initial_vals = self.vals_total();
|
||||
let initial_enabled_vals = self.vals_enabled();
|
||||
|
||||
let password = random_password();
|
||||
let keypair = Keypair::random();
|
||||
let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new())
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
if !s.correct_password {
|
||||
let request = KeystoreValidatorsPostRequest {
|
||||
enable: s.enabled,
|
||||
password: String::from_utf8(random_password().as_ref().to_vec())
|
||||
.unwrap()
|
||||
.into(),
|
||||
keystore,
|
||||
graffiti: None,
|
||||
suggested_fee_recipient: None,
|
||||
gas_limit: None,
|
||||
builder_proposals: None,
|
||||
builder_boost_factor: None,
|
||||
prefer_builder_proposals: None,
|
||||
};
|
||||
|
||||
self.client
|
||||
.post_lighthouse_validators_keystore(&request)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
let request = KeystoreValidatorsPostRequest {
|
||||
enable: s.enabled,
|
||||
password: String::from_utf8(password.as_ref().to_vec())
|
||||
.unwrap()
|
||||
.into(),
|
||||
keystore,
|
||||
graffiti: None,
|
||||
suggested_fee_recipient: None,
|
||||
gas_limit: None,
|
||||
builder_proposals: None,
|
||||
builder_boost_factor: None,
|
||||
prefer_builder_proposals: None,
|
||||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post_lighthouse_validators_keystore(&request)
|
||||
.await
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let num_enabled = s.enabled as usize;
|
||||
|
||||
assert_eq!(self.vals_total(), initial_vals + 1);
|
||||
assert_eq!(self.vals_enabled(), initial_enabled_vals + num_enabled);
|
||||
|
||||
let server_vals = self.client.get_lighthouse_validators().await.unwrap().data;
|
||||
|
||||
assert_eq!(server_vals.len(), self.vals_total());
|
||||
|
||||
assert_eq!(response.voting_pubkey, keypair.pk.into());
|
||||
assert_eq!(response.enabled, s.enabled);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn create_web3signer_validators(self, s: Web3SignerValidatorScenario) -> Self {
|
||||
let initial_vals = self.vals_total();
|
||||
let initial_enabled_vals = self.vals_enabled();
|
||||
|
||||
let request: Vec<_> = (0..s.count)
|
||||
.map(|i| {
|
||||
let kp = Keypair::random();
|
||||
Web3SignerValidatorRequest {
|
||||
enable: s.enabled,
|
||||
description: format!("{}", i),
|
||||
graffiti: None,
|
||||
suggested_fee_recipient: None,
|
||||
gas_limit: None,
|
||||
builder_proposals: None,
|
||||
voting_public_key: kp.pk,
|
||||
url: format!("http://signer_{}.com/", i),
|
||||
root_certificate_path: None,
|
||||
request_timeout_ms: None,
|
||||
client_identity_path: None,
|
||||
client_identity_password: None,
|
||||
builder_boost_factor: None,
|
||||
prefer_builder_proposals: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.client
|
||||
.post_lighthouse_validators_web3signer(&request)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(self.vals_total(), initial_vals + s.count);
|
||||
if s.enabled {
|
||||
assert_eq!(self.vals_enabled(), initial_enabled_vals + s.count);
|
||||
} else {
|
||||
assert_eq!(self.vals_enabled(), initial_enabled_vals);
|
||||
};
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn set_validator_enabled(self, index: usize, enabled: bool) -> Self {
|
||||
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
|
||||
|
||||
self.client
|
||||
.patch_lighthouse_validators(
|
||||
&validator.voting_pubkey,
|
||||
Some(enabled),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
self.initialized_validators
|
||||
.read()
|
||||
.is_enabled(&validator.voting_pubkey.decompress().unwrap())
|
||||
.unwrap(),
|
||||
enabled
|
||||
);
|
||||
|
||||
assert!(self
|
||||
.client
|
||||
.get_lighthouse_validators()
|
||||
.await
|
||||
.unwrap()
|
||||
.data
|
||||
.into_iter()
|
||||
.find(|v| v.voting_pubkey == validator.voting_pubkey)
|
||||
.map(|v| v.enabled == enabled)
|
||||
.unwrap());
|
||||
|
||||
// Check the server via an individual request.
|
||||
assert_eq!(
|
||||
self.client
|
||||
.get_lighthouse_validators_pubkey(&validator.voting_pubkey)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.data
|
||||
.enabled,
|
||||
enabled
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn set_gas_limit(self, index: usize, gas_limit: u64) -> Self {
|
||||
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
|
||||
|
||||
self.client
|
||||
.patch_lighthouse_validators(
|
||||
&validator.voting_pubkey,
|
||||
None,
|
||||
Some(gas_limit),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn assert_gas_limit(self, index: usize, gas_limit: u64) -> Self {
|
||||
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
|
||||
|
||||
assert_eq!(
|
||||
self.validator_store.get_gas_limit(&validator.voting_pubkey),
|
||||
gas_limit
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn set_builder_proposals(self, index: usize, builder_proposals: bool) -> Self {
|
||||
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
|
||||
|
||||
self.client
|
||||
.patch_lighthouse_validators(
|
||||
&validator.voting_pubkey,
|
||||
None,
|
||||
None,
|
||||
Some(builder_proposals),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn assert_builder_proposals(self, index: usize, builder_proposals: bool) -> Self {
|
||||
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
|
||||
|
||||
assert_eq!(
|
||||
self.validator_store
|
||||
.get_builder_proposals(&validator.voting_pubkey),
|
||||
builder_proposals
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,322 +0,0 @@
|
||||
use super::Context;
|
||||
use malloc_utils::scrape_allocator_metrics;
|
||||
use slot_clock::SlotClock;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use types::EthSpec;
|
||||
|
||||
pub const SUCCESS: &str = "success";
|
||||
pub const SLASHABLE: &str = "slashable";
|
||||
pub const SAME_DATA: &str = "same_data";
|
||||
pub const UNREGISTERED: &str = "unregistered";
|
||||
pub const FULL_UPDATE: &str = "full_update";
|
||||
pub const BEACON_BLOCK: &str = "beacon_block";
|
||||
pub const BEACON_BLOCK_HTTP_GET: &str = "beacon_block_http_get";
|
||||
pub const BEACON_BLOCK_HTTP_POST: &str = "beacon_block_http_post";
|
||||
pub const BLINDED_BEACON_BLOCK_HTTP_POST: &str = "blinded_beacon_block_http_post";
|
||||
pub const ATTESTATIONS: &str = "attestations";
|
||||
pub const ATTESTATIONS_HTTP_GET: &str = "attestations_http_get";
|
||||
pub const ATTESTATIONS_HTTP_POST: &str = "attestations_http_post";
|
||||
pub const AGGREGATES: &str = "aggregates";
|
||||
pub const AGGREGATES_HTTP_GET: &str = "aggregates_http_get";
|
||||
pub const AGGREGATES_HTTP_POST: &str = "aggregates_http_post";
|
||||
pub const CURRENT_EPOCH: &str = "current_epoch";
|
||||
pub const NEXT_EPOCH: &str = "next_epoch";
|
||||
pub const UPDATE_INDICES: &str = "update_indices";
|
||||
pub const UPDATE_ATTESTERS_CURRENT_EPOCH: &str = "update_attesters_current_epoch";
|
||||
pub const UPDATE_ATTESTERS_NEXT_EPOCH: &str = "update_attesters_next_epoch";
|
||||
pub const UPDATE_ATTESTERS_FETCH: &str = "update_attesters_fetch";
|
||||
pub const UPDATE_ATTESTERS_STORE: &str = "update_attesters_store";
|
||||
pub const ATTESTER_DUTIES_HTTP_POST: &str = "attester_duties_http_post";
|
||||
pub const PROPOSER_DUTIES_HTTP_GET: &str = "proposer_duties_http_get";
|
||||
pub const VALIDATOR_DUTIES_SYNC_HTTP_POST: &str = "validator_duties_sync_http_post";
|
||||
pub const VALIDATOR_ID_HTTP_GET: &str = "validator_id_http_get";
|
||||
pub const SUBSCRIPTIONS_HTTP_POST: &str = "subscriptions_http_post";
|
||||
pub const UPDATE_PROPOSERS: &str = "update_proposers";
|
||||
pub const ATTESTATION_SELECTION_PROOFS: &str = "attestation_selection_proofs";
|
||||
pub const SUBSCRIPTIONS: &str = "subscriptions";
|
||||
pub const LOCAL_KEYSTORE: &str = "local_keystore";
|
||||
pub const WEB3SIGNER: &str = "web3signer";
|
||||
|
||||
pub use metrics::*;
|
||||
|
||||
pub static GENESIS_DISTANCE: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"vc_genesis_distance_seconds",
|
||||
"Distance between now and genesis time",
|
||||
)
|
||||
});
|
||||
pub static ENABLED_VALIDATORS_COUNT: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"vc_validators_enabled_count",
|
||||
"Number of enabled validators",
|
||||
)
|
||||
});
|
||||
pub static TOTAL_VALIDATORS_COUNT: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"vc_validators_total_count",
|
||||
"Number of total validators (enabled and disabled)",
|
||||
)
|
||||
});
|
||||
|
||||
pub static SIGNED_BLOCKS_TOTAL: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"vc_signed_beacon_blocks_total",
|
||||
"Total count of attempted block signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static SIGNED_ATTESTATIONS_TOTAL: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"vc_signed_attestations_total",
|
||||
"Total count of attempted Attestation signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static SIGNED_AGGREGATES_TOTAL: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"vc_signed_aggregates_total",
|
||||
"Total count of attempted SignedAggregateAndProof signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static SIGNED_SELECTION_PROOFS_TOTAL: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"vc_signed_selection_proofs_total",
|
||||
"Total count of attempted SelectionProof signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL: LazyLock<Result<IntCounterVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"vc_signed_sync_committee_messages_total",
|
||||
"Total count of attempted SyncCommitteeMessage signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL: LazyLock<Result<IntCounterVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"vc_signed_sync_committee_contributions_total",
|
||||
"Total count of attempted ContributionAndProof signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static SIGNED_SYNC_SELECTION_PROOFS_TOTAL: LazyLock<Result<IntCounterVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"vc_signed_sync_selection_proofs_total",
|
||||
"Total count of attempted SyncSelectionProof signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static SIGNED_VOLUNTARY_EXITS_TOTAL: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"vc_signed_voluntary_exits_total",
|
||||
"Total count of VoluntaryExit signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static SIGNED_VALIDATOR_REGISTRATIONS_TOTAL: LazyLock<Result<IntCounterVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"builder_validator_registrations_total",
|
||||
"Total count of ValidatorRegistrationData signings",
|
||||
&["status"],
|
||||
)
|
||||
});
|
||||
pub static DUTIES_SERVICE_TIMES: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"vc_duties_service_task_times_seconds",
|
||||
"Duration to perform duties service tasks",
|
||||
&["task"],
|
||||
)
|
||||
});
|
||||
pub static ATTESTATION_SERVICE_TIMES: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"vc_attestation_service_task_times_seconds",
|
||||
"Duration to perform attestation service tasks",
|
||||
&["task"],
|
||||
)
|
||||
});
|
||||
pub static SLASHING_PROTECTION_PRUNE_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"vc_slashing_protection_prune_times_seconds",
|
||||
"Time required to prune the slashing protection DB",
|
||||
)
|
||||
});
|
||||
pub static BLOCK_SERVICE_TIMES: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"vc_beacon_block_service_task_times_seconds",
|
||||
"Duration to perform beacon block service tasks",
|
||||
&["task"],
|
||||
)
|
||||
});
|
||||
pub static PROPOSER_COUNT: LazyLock<Result<IntGaugeVec>> = LazyLock::new(|| {
|
||||
try_create_int_gauge_vec(
|
||||
"vc_beacon_block_proposer_count",
|
||||
"Number of beacon block proposers on this host",
|
||||
&["task"],
|
||||
)
|
||||
});
|
||||
pub static ATTESTER_COUNT: LazyLock<Result<IntGaugeVec>> = LazyLock::new(|| {
|
||||
try_create_int_gauge_vec(
|
||||
"vc_beacon_attester_count",
|
||||
"Number of attesters on this host",
|
||||
&["task"],
|
||||
)
|
||||
});
|
||||
pub static PROPOSAL_CHANGED: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"vc_beacon_block_proposal_changed",
|
||||
"A duties update discovered a new block proposer for the current slot",
|
||||
)
|
||||
});
|
||||
/*
|
||||
* Endpoint metrics
|
||||
*/
|
||||
pub static ENDPOINT_ERRORS: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"bn_endpoint_errors",
|
||||
"The number of beacon node request errors for each endpoint",
|
||||
&["endpoint"],
|
||||
)
|
||||
});
|
||||
pub static ENDPOINT_REQUESTS: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| {
|
||||
try_create_int_counter_vec(
|
||||
"bn_endpoint_requests",
|
||||
"The number of beacon node requests for each endpoint",
|
||||
&["endpoint"],
|
||||
)
|
||||
});
|
||||
|
||||
/*
|
||||
* Beacon node availability metrics
|
||||
*/
|
||||
pub static AVAILABLE_BEACON_NODES_COUNT: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"vc_beacon_nodes_available_count",
|
||||
"Number of available beacon nodes",
|
||||
)
|
||||
});
|
||||
pub static SYNCED_BEACON_NODES_COUNT: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"vc_beacon_nodes_synced_count",
|
||||
"Number of synced beacon nodes",
|
||||
)
|
||||
});
|
||||
pub static TOTAL_BEACON_NODES_COUNT: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"vc_beacon_nodes_total_count",
|
||||
"Total number of beacon nodes",
|
||||
)
|
||||
});
|
||||
|
||||
pub static ETH2_FALLBACK_CONFIGURED: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"sync_eth2_fallback_configured",
|
||||
"The number of configured eth2 fallbacks",
|
||||
)
|
||||
});
|
||||
|
||||
pub static ETH2_FALLBACK_CONNECTED: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"sync_eth2_fallback_connected",
|
||||
"Set to 1 if connected to atleast one synced eth2 fallback node, otherwise set to 0",
|
||||
)
|
||||
});
|
||||
/*
|
||||
* Signing Metrics
|
||||
*/
|
||||
pub static SIGNING_TIMES: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"vc_signing_times_seconds",
|
||||
"Duration to obtain a signature",
|
||||
&["type"],
|
||||
)
|
||||
});
|
||||
pub static BLOCK_SIGNING_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"vc_block_signing_times_seconds",
|
||||
"Duration to obtain a signature for a block",
|
||||
)
|
||||
});
|
||||
|
||||
pub static ATTESTATION_DUTY: LazyLock<Result<IntGaugeVec>> = LazyLock::new(|| {
|
||||
try_create_int_gauge_vec(
|
||||
"vc_attestation_duty_slot",
|
||||
"Attestation duty slot for all managed validators",
|
||||
&["validator"],
|
||||
)
|
||||
});
|
||||
/*
|
||||
* BN latency
|
||||
*/
|
||||
pub static VC_BEACON_NODE_LATENCY: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"vc_beacon_node_latency",
|
||||
"Round-trip latency for a simple API endpoint on each BN",
|
||||
&["endpoint"],
|
||||
)
|
||||
});
|
||||
pub static VC_BEACON_NODE_LATENCY_PRIMARY_ENDPOINT: LazyLock<Result<Histogram>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"vc_beacon_node_latency_primary_endpoint",
|
||||
"Round-trip latency for the primary BN endpoint",
|
||||
)
|
||||
});
|
||||
|
||||
pub fn gather_prometheus_metrics<E: EthSpec>(
|
||||
ctx: &Context<E>,
|
||||
) -> std::result::Result<String, String> {
|
||||
let mut buffer = vec![];
|
||||
let encoder = TextEncoder::new();
|
||||
|
||||
{
|
||||
let shared = ctx.shared.read();
|
||||
|
||||
if let Some(genesis_time) = shared.genesis_time {
|
||||
if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
let distance = now.as_secs() as i64 - genesis_time as i64;
|
||||
set_gauge(&GENESIS_DISTANCE, distance);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(duties_service) = &shared.duties_service {
|
||||
if let Some(slot) = duties_service.slot_clock.now() {
|
||||
let current_epoch = slot.epoch(E::slots_per_epoch());
|
||||
let next_epoch = current_epoch + 1;
|
||||
|
||||
set_int_gauge(
|
||||
&PROPOSER_COUNT,
|
||||
&[CURRENT_EPOCH],
|
||||
duties_service.proposer_count(current_epoch) as i64,
|
||||
);
|
||||
set_int_gauge(
|
||||
&ATTESTER_COUNT,
|
||||
&[CURRENT_EPOCH],
|
||||
duties_service.attester_count(current_epoch) as i64,
|
||||
);
|
||||
set_int_gauge(
|
||||
&ATTESTER_COUNT,
|
||||
&[NEXT_EPOCH],
|
||||
duties_service.attester_count(next_epoch) as i64,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's important to ensure these metrics are explicitly enabled in the case that users aren't
|
||||
// using glibc and this function causes panics.
|
||||
if ctx.config.allocator_metrics_enabled {
|
||||
scrape_allocator_metrics();
|
||||
}
|
||||
|
||||
warp_utils::metrics::scrape_health_metrics();
|
||||
|
||||
encoder.encode(&metrics::gather(), &mut buffer).unwrap();
|
||||
|
||||
String::from_utf8(buffer).map_err(|e| format!("Failed to encode prometheus info: {:?}", e))
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
//! This crate provides a HTTP server that is solely dedicated to serving the `/metrics` endpoint.
|
||||
//!
|
||||
//! For other endpoints, see the `http_api` crate.
|
||||
pub mod metrics;
|
||||
|
||||
use crate::{DutiesService, ValidatorStore};
|
||||
use lighthouse_version::version_with_platform;
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use slog::{crit, info, Logger};
|
||||
use slot_clock::SystemTimeSlotClock;
|
||||
use std::future::Future;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use types::EthSpec;
|
||||
use warp::{http::Response, Filter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Warp(#[allow(dead_code)] warp::Error),
|
||||
Other(#[allow(dead_code)] String),
|
||||
}
|
||||
|
||||
impl From<warp::Error> for Error {
|
||||
fn from(e: warp::Error) -> Self {
|
||||
Error::Warp(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(e: String) -> Self {
|
||||
Error::Other(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains objects which have shared access from inside/outside of the metrics server.
|
||||
pub struct Shared<E: EthSpec> {
|
||||
pub validator_store: Option<Arc<ValidatorStore<SystemTimeSlotClock, E>>>,
|
||||
pub duties_service: Option<Arc<DutiesService<SystemTimeSlotClock, E>>>,
|
||||
pub genesis_time: Option<u64>,
|
||||
}
|
||||
|
||||
/// 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<E: EthSpec> {
|
||||
pub config: Config,
|
||||
pub shared: RwLock<Shared<E>>,
|
||||
pub log: Logger,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
pub allocator_metrics_enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
listen_port: 5064,
|
||||
allow_origin: None,
|
||||
allocator_metrics_enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<E: EthSpec>(
|
||||
ctx: Arc<Context<E>>,
|
||||
shutdown: impl Future<Output = ()> + Send + Sync + 'static,
|
||||
) -> Result<(SocketAddr, impl Future<Output = ()>), Error> {
|
||||
let config = &ctx.config;
|
||||
let log = ctx.log.clone();
|
||||
|
||||
// Configure CORS.
|
||||
let cors_builder = {
|
||||
let builder = warp::cors()
|
||||
.allow_method("GET")
|
||||
.allow_headers(vec!["Content-Type"]);
|
||||
|
||||
warp_utils::cors::set_builder_origins(
|
||||
builder,
|
||||
config.allow_origin.as_deref(),
|
||||
(config.listen_addr, config.listen_port),
|
||||
)?
|
||||
};
|
||||
|
||||
// Sanity check.
|
||||
if !config.enabled {
|
||||
crit!(log, "Cannot start disabled metrics HTTP server");
|
||||
return Err(Error::Other(
|
||||
"A disabled metrics server should not be started".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let inner_ctx = ctx.clone();
|
||||
let routes = warp::get()
|
||||
.and(warp::path("metrics"))
|
||||
.map(move || inner_ctx.clone())
|
||||
.and_then(|ctx: Arc<Context<E>>| async move {
|
||||
Ok::<_, warp::Rejection>(
|
||||
metrics::gather_prometheus_metrics(&ctx)
|
||||
.map(|body| {
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(body)
|
||||
.unwrap()
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(format!("Unable to gather metrics: {:?}", e))
|
||||
.unwrap()
|
||||
}),
|
||||
)
|
||||
})
|
||||
// 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!(
|
||||
log,
|
||||
"Metrics HTTP server started";
|
||||
"listen_address" => listening_socket.to_string(),
|
||||
);
|
||||
|
||||
Ok((listening_socket, server))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,354 +0,0 @@
|
||||
use account_utils::write_file_via_temporary;
|
||||
use bls::{Keypair, PublicKey};
|
||||
use eth2_keystore::json_keystore::{
|
||||
Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, KdfModule,
|
||||
Sha256Checksum,
|
||||
};
|
||||
use eth2_keystore::{
|
||||
decrypt, default_kdf, encrypt, keypair_from_secret, Error as KeystoreError, PlainText, Uuid,
|
||||
ZeroizeHash, IV_SIZE, SALT_SIZE,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The file name for the serialized `KeyCache` struct.
|
||||
pub const CACHE_FILENAME: &str = "validator_key_cache.json";
|
||||
|
||||
/// The file name for the temporary `KeyCache`.
|
||||
pub const TEMP_CACHE_FILENAME: &str = ".validator_key_cache.json.tmp";
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum State {
|
||||
NotDecrypted,
|
||||
DecryptedAndSaved,
|
||||
DecryptedWithUnsavedUpdates,
|
||||
}
|
||||
|
||||
fn not_decrypted() -> State {
|
||||
State::NotDecrypted
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct KeyCache {
|
||||
crypto: Crypto,
|
||||
uuids: Vec<Uuid>,
|
||||
#[serde(skip)]
|
||||
pairs: HashMap<Uuid, Keypair>, //maps public keystore uuids to their corresponding Keypair
|
||||
#[serde(skip)]
|
||||
passwords: Vec<PlainText>,
|
||||
#[serde(skip)]
|
||||
#[serde(default = "not_decrypted")]
|
||||
state: State,
|
||||
}
|
||||
|
||||
type SerializedKeyMap = HashMap<Uuid, ZeroizeHash>;
|
||||
|
||||
impl Default for KeyCache {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyCache {
|
||||
pub fn new() -> Self {
|
||||
KeyCache {
|
||||
uuids: Vec::new(),
|
||||
crypto: Self::init_crypto(),
|
||||
pairs: HashMap::new(),
|
||||
passwords: Vec::new(),
|
||||
state: State::DecryptedWithUnsavedUpdates,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_crypto() -> Crypto {
|
||||
let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>();
|
||||
let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into();
|
||||
|
||||
let kdf = default_kdf(salt.to_vec());
|
||||
let cipher = Cipher::Aes128Ctr(Aes128Ctr { iv });
|
||||
|
||||
Crypto {
|
||||
kdf: KdfModule {
|
||||
function: kdf.function(),
|
||||
params: kdf,
|
||||
message: EmptyString,
|
||||
},
|
||||
checksum: ChecksumModule {
|
||||
function: Sha256Checksum::function(),
|
||||
params: EmptyMap,
|
||||
message: Vec::new().into(),
|
||||
},
|
||||
cipher: CipherModule {
|
||||
function: cipher.function(),
|
||||
params: cipher,
|
||||
message: Vec::new().into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_file_path<P: AsRef<Path>>(validators_dir: P) -> PathBuf {
|
||||
validators_dir.as_ref().join(CACHE_FILENAME)
|
||||
}
|
||||
|
||||
/// Open an existing file or create a new, empty one if it does not exist.
|
||||
pub fn open_or_create<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> {
|
||||
let cache_path = Self::cache_file_path(validators_dir.as_ref());
|
||||
if !cache_path.exists() {
|
||||
Ok(Self::new())
|
||||
} else {
|
||||
Self::open(validators_dir)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open an existing file, returning an error if the file does not exist.
|
||||
pub fn open<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> {
|
||||
let cache_path = validators_dir.as_ref().join(CACHE_FILENAME);
|
||||
let file = File::options()
|
||||
.read(true)
|
||||
.create_new(false)
|
||||
.open(cache_path)
|
||||
.map_err(Error::UnableToOpenFile)?;
|
||||
serde_json::from_reader(file).map_err(Error::UnableToParseFile)
|
||||
}
|
||||
|
||||
fn encrypt(&mut self) -> Result<(), Error> {
|
||||
self.crypto = Self::init_crypto();
|
||||
let secret_map: SerializedKeyMap = self
|
||||
.pairs
|
||||
.iter()
|
||||
.map(|(k, v)| (*k, v.sk.serialize()))
|
||||
.collect();
|
||||
|
||||
let raw = PlainText::from(
|
||||
bincode::serialize(&secret_map).map_err(Error::UnableToSerializeKeyMap)?,
|
||||
);
|
||||
let (cipher_text, checksum) = encrypt(
|
||||
raw.as_ref(),
|
||||
Self::password(&self.passwords).as_ref(),
|
||||
&self.crypto.kdf.params,
|
||||
&self.crypto.cipher.params,
|
||||
)
|
||||
.map_err(Error::UnableToEncrypt)?;
|
||||
|
||||
self.crypto.cipher.message = cipher_text.into();
|
||||
self.crypto.checksum.message = checksum.to_vec().into();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores `Self` encrypted in json format.
|
||||
///
|
||||
/// Will create a new file if it does not exist or over-write any existing file.
|
||||
/// Returns false iff there are no unsaved changes
|
||||
pub fn save<P: AsRef<Path>>(&mut self, validators_dir: P) -> Result<bool, Error> {
|
||||
if self.is_modified() {
|
||||
self.encrypt()?;
|
||||
|
||||
let cache_path = validators_dir.as_ref().join(CACHE_FILENAME);
|
||||
let temp_path = validators_dir.as_ref().join(TEMP_CACHE_FILENAME);
|
||||
let bytes = serde_json::to_vec(self).map_err(Error::UnableToEncodeFile)?;
|
||||
|
||||
write_file_via_temporary(&cache_path, &temp_path, &bytes)
|
||||
.map_err(Error::UnableToCreateFile)?;
|
||||
|
||||
self.state = State::DecryptedAndSaved;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_modified(&self) -> bool {
|
||||
self.state == State::DecryptedWithUnsavedUpdates
|
||||
}
|
||||
|
||||
pub fn uuids(&self) -> &Vec<Uuid> {
|
||||
&self.uuids
|
||||
}
|
||||
|
||||
fn password(passwords: &[PlainText]) -> PlainText {
|
||||
PlainText::from(passwords.iter().fold(Vec::new(), |mut v, p| {
|
||||
v.extend(p.as_ref());
|
||||
v
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn decrypt(
|
||||
&mut self,
|
||||
passwords: Vec<PlainText>,
|
||||
public_keys: Vec<PublicKey>,
|
||||
) -> Result<&HashMap<Uuid, Keypair>, Error> {
|
||||
match self.state {
|
||||
State::NotDecrypted => {
|
||||
let password = Self::password(&passwords);
|
||||
let text =
|
||||
decrypt(password.as_ref(), &self.crypto).map_err(Error::UnableToDecrypt)?;
|
||||
let key_map: SerializedKeyMap =
|
||||
bincode::deserialize(text.as_bytes()).map_err(Error::UnableToParseKeyMap)?;
|
||||
self.passwords = passwords;
|
||||
self.pairs = HashMap::new();
|
||||
if public_keys.len() != self.uuids.len() {
|
||||
return Err(Error::PublicKeyMismatch);
|
||||
}
|
||||
for (uuid, public_key) in self.uuids.iter().zip(public_keys.iter()) {
|
||||
if let Some(secret) = key_map.get(uuid) {
|
||||
let key_pair = keypair_from_secret(secret.as_ref())
|
||||
.map_err(Error::UnableToParseKeyPair)?;
|
||||
if &key_pair.pk != public_key {
|
||||
return Err(Error::PublicKeyMismatch);
|
||||
}
|
||||
self.pairs.insert(*uuid, key_pair);
|
||||
} else {
|
||||
return Err(Error::MissingUuidKey);
|
||||
}
|
||||
}
|
||||
self.state = State::DecryptedAndSaved;
|
||||
Ok(&self.pairs)
|
||||
}
|
||||
_ => Err(Error::AlreadyDecrypted),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, uuid: &Uuid) {
|
||||
//do nothing in not decrypted state
|
||||
if let State::NotDecrypted = self.state {
|
||||
return;
|
||||
}
|
||||
self.pairs.remove(uuid);
|
||||
if let Some(pos) = self.uuids.iter().position(|uuid2| uuid2 == uuid) {
|
||||
self.uuids.remove(pos);
|
||||
self.passwords.remove(pos);
|
||||
}
|
||||
self.state = State::DecryptedWithUnsavedUpdates;
|
||||
}
|
||||
|
||||
pub fn add(&mut self, keypair: Keypair, uuid: &Uuid, password: PlainText) {
|
||||
//do nothing in not decrypted state
|
||||
if let State::NotDecrypted = self.state {
|
||||
return;
|
||||
}
|
||||
self.pairs.insert(*uuid, keypair);
|
||||
self.uuids.push(*uuid);
|
||||
self.passwords.push(password);
|
||||
self.state = State::DecryptedWithUnsavedUpdates;
|
||||
}
|
||||
|
||||
pub fn get(&self, uuid: &Uuid) -> Option<Keypair> {
|
||||
self.pairs.get(uuid).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// The cache file could not be opened.
|
||||
UnableToOpenFile(io::Error),
|
||||
/// The cache file could not be parsed as JSON.
|
||||
UnableToParseFile(serde_json::Error),
|
||||
/// The cache file could not be serialized as YAML.
|
||||
UnableToEncodeFile(serde_json::Error),
|
||||
/// The cache file or its temporary could not be written to the filesystem.
|
||||
UnableToWriteFile(io::Error),
|
||||
UnableToCreateFile(filesystem::Error),
|
||||
/// Couldn't decrypt the cache file
|
||||
UnableToDecrypt(KeystoreError),
|
||||
UnableToEncrypt(KeystoreError),
|
||||
/// Couldn't decode the decrypted hashmap
|
||||
UnableToParseKeyMap(bincode::Error),
|
||||
UnableToParseKeyPair(KeystoreError),
|
||||
UnableToSerializeKeyMap(bincode::Error),
|
||||
PublicKeyMismatch,
|
||||
MissingUuidKey,
|
||||
/// Cache file is already decrypted
|
||||
AlreadyDecrypted,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use eth2_keystore::json_keystore::{HexBytes, Kdf};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeyCacheTest {
|
||||
pub params: Kdf,
|
||||
//pub checksum: ChecksumModule,
|
||||
//pub cipher: CipherModule,
|
||||
uuids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serialization() {
|
||||
let mut key_cache = KeyCache::new();
|
||||
let key_pair = Keypair::random();
|
||||
let uuid = Uuid::from_u128(1);
|
||||
let password = PlainText::from(vec![1, 2, 3, 4, 5, 6]);
|
||||
key_cache.add(key_pair, &uuid, password);
|
||||
|
||||
key_cache.crypto.cipher.message = HexBytes::from(vec![7, 8, 9]);
|
||||
key_cache.crypto.checksum.message = HexBytes::from(vec![10, 11, 12]);
|
||||
|
||||
let binary = serde_json::to_vec(&key_cache).unwrap();
|
||||
let clone: KeyCache = serde_json::from_slice(binary.as_ref()).unwrap();
|
||||
|
||||
assert_eq!(clone.crypto, key_cache.crypto);
|
||||
assert_eq!(clone.uuids, key_cache.uuids);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_encryption() {
|
||||
let mut key_cache = KeyCache::new();
|
||||
let keypairs = vec![Keypair::random(), Keypair::random()];
|
||||
let uuids = vec![Uuid::from_u128(1), Uuid::from_u128(2)];
|
||||
let passwords = vec![
|
||||
PlainText::from(vec![1, 2, 3, 4, 5, 6]),
|
||||
PlainText::from(vec![7, 8, 9, 10, 11, 12]),
|
||||
];
|
||||
|
||||
for ((keypair, uuid), password) in keypairs.iter().zip(uuids.iter()).zip(passwords.iter()) {
|
||||
key_cache.add(keypair.clone(), uuid, password.clone());
|
||||
}
|
||||
|
||||
key_cache.encrypt().unwrap();
|
||||
key_cache.state = State::DecryptedAndSaved;
|
||||
|
||||
assert_eq!(&key_cache.uuids, &uuids);
|
||||
|
||||
let mut new_clone = KeyCache {
|
||||
crypto: key_cache.crypto.clone(),
|
||||
uuids: key_cache.uuids.clone(),
|
||||
pairs: Default::default(),
|
||||
passwords: vec![],
|
||||
state: State::NotDecrypted,
|
||||
};
|
||||
|
||||
new_clone
|
||||
.decrypt(passwords, keypairs.iter().map(|p| p.pk.clone()).collect())
|
||||
.unwrap();
|
||||
|
||||
let passwords_to_plain = |cache: &KeyCache| -> Vec<Vec<u8>> {
|
||||
cache
|
||||
.passwords
|
||||
.iter()
|
||||
.map(|x| x.as_bytes().to_vec())
|
||||
.collect()
|
||||
};
|
||||
|
||||
assert_eq!(key_cache.crypto, new_clone.crypto);
|
||||
assert_eq!(
|
||||
passwords_to_plain(&key_cache),
|
||||
passwords_to_plain(&new_clone)
|
||||
);
|
||||
assert_eq!(key_cache.uuids, new_clone.uuids);
|
||||
assert_eq!(key_cache.state, new_clone.state);
|
||||
assert_eq!(key_cache.pairs.len(), new_clone.pairs.len());
|
||||
for (key, value) in key_cache.pairs {
|
||||
assert!(new_clone.pairs.contains_key(&key));
|
||||
assert_eq!(
|
||||
format!("{:?}", value),
|
||||
format!("{:?}", new_clone.pairs[&key])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{http_metrics::metrics, BeaconNodeFallback};
|
||||
use beacon_node_fallback::BeaconNodeFallback;
|
||||
use environment::RuntimeContext;
|
||||
use slog::debug;
|
||||
use slot_clock::SlotClock;
|
||||
@@ -44,14 +44,14 @@ pub fn start_latency_service<T: SlotClock + 'static, E: EthSpec>(
|
||||
"node" => &measurement.beacon_node_id,
|
||||
"latency" => latency.as_millis(),
|
||||
);
|
||||
metrics::observe_timer_vec(
|
||||
&metrics::VC_BEACON_NODE_LATENCY,
|
||||
validator_metrics::observe_timer_vec(
|
||||
&validator_metrics::VC_BEACON_NODE_LATENCY,
|
||||
&[&measurement.beacon_node_id],
|
||||
latency,
|
||||
);
|
||||
if i == 0 {
|
||||
metrics::observe_duration(
|
||||
&metrics::VC_BEACON_NODE_LATENCY_PRIMARY_ENDPOINT,
|
||||
validator_metrics::observe_duration(
|
||||
&validator_metrics::VC_BEACON_NODE_LATENCY_PRIMARY_ENDPOINT,
|
||||
latency,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,28 @@
|
||||
mod attestation_service;
|
||||
mod beacon_node_fallback;
|
||||
mod beacon_node_health;
|
||||
mod block_service;
|
||||
mod check_synced;
|
||||
mod cli;
|
||||
mod duties_service;
|
||||
mod graffiti_file;
|
||||
mod http_metrics;
|
||||
mod key_cache;
|
||||
pub mod config;
|
||||
mod latency;
|
||||
mod notifier;
|
||||
mod preparation_service;
|
||||
mod signing_method;
|
||||
mod sync_committee_service;
|
||||
|
||||
pub mod config;
|
||||
mod doppelganger_service;
|
||||
pub mod http_api;
|
||||
pub mod initialized_validators;
|
||||
pub mod validator_store;
|
||||
|
||||
pub use beacon_node_fallback::ApiTopic;
|
||||
pub use beacon_node_health::BeaconNodeSyncDistanceTiers;
|
||||
pub use cli::cli_app;
|
||||
pub use config::Config;
|
||||
use initialized_validators::InitializedValidators;
|
||||
use metrics::set_gauge;
|
||||
use monitoring_api::{MonitoringHttpClient, ProcessType};
|
||||
use sensitive_url::SensitiveUrl;
|
||||
pub use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
|
||||
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
|
||||
|
||||
use crate::beacon_node_fallback::{
|
||||
use beacon_node_fallback::{
|
||||
start_fallback_updater_service, BeaconNodeFallback, CandidateBeaconNode,
|
||||
};
|
||||
use crate::doppelganger_service::DoppelgangerService;
|
||||
use crate::graffiti_file::GraffitiFile;
|
||||
use crate::initialized_validators::Error::UnableToOpenVotingKeystore;
|
||||
|
||||
use account_utils::validator_definitions::ValidatorDefinitions;
|
||||
use attestation_service::{AttestationService, AttestationServiceBuilder};
|
||||
use block_service::{BlockService, BlockServiceBuilder};
|
||||
use clap::ArgMatches;
|
||||
use duties_service::{sync::SyncDutiesMap, DutiesService};
|
||||
use doppelganger_service::DoppelgangerService;
|
||||
use environment::RuntimeContext;
|
||||
use eth2::{reqwest::ClientBuilder, types::Graffiti, BeaconNodeHttpClient, StatusCode, Timeouts};
|
||||
use http_api::ApiSecret;
|
||||
use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, StatusCode, Timeouts};
|
||||
use initialized_validators::Error::UnableToOpenVotingKeystore;
|
||||
use notifier::spawn_notifier;
|
||||
use parking_lot::RwLock;
|
||||
use preparation_service::{PreparationService, PreparationServiceBuilder};
|
||||
use reqwest::Certificate;
|
||||
use slog::{debug, error, info, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
@@ -58,12 +34,20 @@ use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use sync_committee_service::SyncCommitteeService;
|
||||
use tokio::{
|
||||
sync::mpsc,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
use types::{EthSpec, Hash256, PublicKeyBytes};
|
||||
use types::{EthSpec, Hash256};
|
||||
use validator_http_api::ApiSecret;
|
||||
use validator_services::{
|
||||
attestation_service::{AttestationService, AttestationServiceBuilder},
|
||||
block_service::{BlockService, BlockServiceBuilder},
|
||||
duties_service::{self, DutiesService},
|
||||
preparation_service::{PreparationService, PreparationServiceBuilder},
|
||||
sync::SyncDutiesMap,
|
||||
sync_committee_service::SyncCommitteeService,
|
||||
};
|
||||
use validator_store::ValidatorStore;
|
||||
|
||||
/// The interval between attempts to contact the beacon node during startup.
|
||||
@@ -152,22 +136,23 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
||||
);
|
||||
|
||||
// Optionally start the metrics server.
|
||||
let http_metrics_ctx = if config.http_metrics.enabled {
|
||||
let shared = http_metrics::Shared {
|
||||
let validator_metrics_ctx = if config.http_metrics.enabled {
|
||||
let shared = validator_http_metrics::Shared {
|
||||
validator_store: None,
|
||||
genesis_time: None,
|
||||
duties_service: None,
|
||||
};
|
||||
|
||||
let ctx: Arc<http_metrics::Context<E>> = Arc::new(http_metrics::Context {
|
||||
config: config.http_metrics.clone(),
|
||||
shared: RwLock::new(shared),
|
||||
log: log.clone(),
|
||||
});
|
||||
let ctx: Arc<validator_http_metrics::Context<E>> =
|
||||
Arc::new(validator_http_metrics::Context {
|
||||
config: config.http_metrics.clone(),
|
||||
shared: RwLock::new(shared),
|
||||
log: log.clone(),
|
||||
});
|
||||
|
||||
let exit = context.executor.exit();
|
||||
|
||||
let (_listen_addr, server) = http_metrics::serve(ctx.clone(), exit)
|
||||
let (_listen_addr, server) = validator_http_metrics::serve(ctx.clone(), exit)
|
||||
.map_err(|e| format!("Unable to start metrics API server: {:?}", e))?;
|
||||
|
||||
context
|
||||
@@ -215,7 +200,7 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
||||
let validators = InitializedValidators::from_definitions(
|
||||
validator_defs,
|
||||
config.validator_dir.clone(),
|
||||
config.clone(),
|
||||
config.initialized_validators.clone(),
|
||||
log.clone(),
|
||||
)
|
||||
.await
|
||||
@@ -384,20 +369,20 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
||||
|
||||
// Set the count for beacon node fallbacks excluding the primary beacon node.
|
||||
set_gauge(
|
||||
&http_metrics::metrics::ETH2_FALLBACK_CONFIGURED,
|
||||
&validator_metrics::ETH2_FALLBACK_CONFIGURED,
|
||||
num_nodes.saturating_sub(1) as i64,
|
||||
);
|
||||
// Set the total beacon node count.
|
||||
set_gauge(
|
||||
&http_metrics::metrics::TOTAL_BEACON_NODES_COUNT,
|
||||
&validator_metrics::TOTAL_BEACON_NODES_COUNT,
|
||||
num_nodes as i64,
|
||||
);
|
||||
|
||||
// Initialize the number of connected, synced beacon nodes to 0.
|
||||
set_gauge(&http_metrics::metrics::ETH2_FALLBACK_CONNECTED, 0);
|
||||
set_gauge(&http_metrics::metrics::SYNCED_BEACON_NODES_COUNT, 0);
|
||||
set_gauge(&validator_metrics::ETH2_FALLBACK_CONNECTED, 0);
|
||||
set_gauge(&validator_metrics::SYNCED_BEACON_NODES_COUNT, 0);
|
||||
// Initialize the number of connected, avaliable beacon nodes to 0.
|
||||
set_gauge(&http_metrics::metrics::AVAILABLE_BEACON_NODES_COUNT, 0);
|
||||
set_gauge(&validator_metrics::AVAILABLE_BEACON_NODES_COUNT, 0);
|
||||
|
||||
let mut beacon_nodes: BeaconNodeFallback<_, E> = BeaconNodeFallback::new(
|
||||
candidates,
|
||||
@@ -422,7 +407,7 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
||||
};
|
||||
|
||||
// Update the metrics server.
|
||||
if let Some(ctx) = &http_metrics_ctx {
|
||||
if let Some(ctx) = &validator_metrics_ctx {
|
||||
ctx.shared.write().genesis_time = Some(genesis_time);
|
||||
}
|
||||
|
||||
@@ -459,7 +444,7 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
||||
context.eth2_config.spec.clone(),
|
||||
doppelganger_service.clone(),
|
||||
slot_clock.clone(),
|
||||
&config,
|
||||
&config.validator_store,
|
||||
context.executor.clone(),
|
||||
log.clone(),
|
||||
));
|
||||
@@ -496,7 +481,7 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
||||
});
|
||||
|
||||
// Update the metrics server.
|
||||
if let Some(ctx) = &http_metrics_ctx {
|
||||
if let Some(ctx) = &validator_metrics_ctx {
|
||||
ctx.shared.write().validator_store = Some(validator_store.clone());
|
||||
ctx.shared.write().duties_service = Some(duties_service.clone());
|
||||
}
|
||||
@@ -569,7 +554,7 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
||||
let api_secret = ApiSecret::create_or_open(&self.config.validator_dir)?;
|
||||
|
||||
self.http_api_listen_addr = if self.config.http_api.enabled {
|
||||
let ctx = Arc::new(http_api::Context {
|
||||
let ctx = Arc::new(validator_http_api::Context {
|
||||
task_executor: self.context.executor.clone(),
|
||||
api_secret,
|
||||
block_service: Some(self.block_service.clone()),
|
||||
@@ -588,7 +573,7 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
|
||||
|
||||
let exit = self.context.executor.exit();
|
||||
|
||||
let (listen_addr, server) = http_api::serve(ctx, exit)
|
||||
let (listen_addr, server) = validator_http_api::serve(ctx, exit)
|
||||
.map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?;
|
||||
|
||||
self.context
|
||||
@@ -850,24 +835,3 @@ pub fn load_pem_certificate<P: AsRef<Path>>(pem_path: P) -> Result<Certificate,
|
||||
.map_err(|e| format!("Unable to read certificate file: {}", e))?;
|
||||
Certificate::from_pem(&buf).map_err(|e| format!("Unable to parse certificate: {}", e))
|
||||
}
|
||||
|
||||
// Given the various graffiti control methods, determine the graffiti that will be used for
|
||||
// the next block produced by the validator with the given public key.
|
||||
pub fn determine_graffiti(
|
||||
validator_pubkey: &PublicKeyBytes,
|
||||
log: &Logger,
|
||||
graffiti_file: Option<GraffitiFile>,
|
||||
validator_definition_graffiti: Option<Graffiti>,
|
||||
graffiti_flag: Option<Graffiti>,
|
||||
) -> Option<Graffiti> {
|
||||
graffiti_file
|
||||
.and_then(|mut g| match g.load_graffiti(validator_pubkey) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
warn!(log, "Failed to read graffiti file"; "error" => ?e);
|
||||
None
|
||||
}
|
||||
})
|
||||
.or(validator_definition_graffiti)
|
||||
.or(graffiti_flag)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::http_metrics;
|
||||
use crate::{DutiesService, ProductionValidatorClient};
|
||||
use metrics::set_gauge;
|
||||
use slog::{debug, error, info, Logger};
|
||||
@@ -45,15 +44,15 @@ async fn notify<T: SlotClock + 'static, E: EthSpec>(
|
||||
let num_synced_fallback = num_synced.saturating_sub(1);
|
||||
|
||||
set_gauge(
|
||||
&http_metrics::metrics::AVAILABLE_BEACON_NODES_COUNT,
|
||||
&validator_metrics::AVAILABLE_BEACON_NODES_COUNT,
|
||||
num_available as i64,
|
||||
);
|
||||
set_gauge(
|
||||
&http_metrics::metrics::SYNCED_BEACON_NODES_COUNT,
|
||||
&validator_metrics::SYNCED_BEACON_NODES_COUNT,
|
||||
num_synced as i64,
|
||||
);
|
||||
set_gauge(
|
||||
&http_metrics::metrics::TOTAL_BEACON_NODES_COUNT,
|
||||
&validator_metrics::TOTAL_BEACON_NODES_COUNT,
|
||||
num_total as i64,
|
||||
);
|
||||
if num_synced > 0 {
|
||||
@@ -79,9 +78,9 @@ async fn notify<T: SlotClock + 'static, E: EthSpec>(
|
||||
)
|
||||
}
|
||||
if num_synced_fallback > 0 {
|
||||
set_gauge(&http_metrics::metrics::ETH2_FALLBACK_CONNECTED, 1);
|
||||
set_gauge(&validator_metrics::ETH2_FALLBACK_CONNECTED, 1);
|
||||
} else {
|
||||
set_gauge(&http_metrics::metrics::ETH2_FALLBACK_CONNECTED, 0);
|
||||
set_gauge(&validator_metrics::ETH2_FALLBACK_CONNECTED, 0);
|
||||
}
|
||||
|
||||
for info in candidate_info {
|
||||
|
||||
@@ -1,502 +0,0 @@
|
||||
use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback};
|
||||
use crate::validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore};
|
||||
use bls::PublicKeyBytes;
|
||||
use environment::RuntimeContext;
|
||||
use parking_lot::RwLock;
|
||||
use slog::{debug, error, info, warn};
|
||||
use slot_clock::SlotClock;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use types::{
|
||||
Address, ChainSpec, EthSpec, ProposerPreparationData, SignedValidatorRegistrationData,
|
||||
ValidatorRegistrationData,
|
||||
};
|
||||
|
||||
/// Number of epochs before the Bellatrix hard fork to begin posting proposer preparations.
|
||||
const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2;
|
||||
|
||||
/// Number of epochs to wait before re-submitting validator registration.
|
||||
const EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION: u64 = 1;
|
||||
|
||||
/// Builds an `PreparationService`.
|
||||
pub struct PreparationServiceBuilder<T: SlotClock + 'static, E: EthSpec> {
|
||||
validator_store: Option<Arc<ValidatorStore<T, E>>>,
|
||||
slot_clock: Option<T>,
|
||||
beacon_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
|
||||
context: Option<RuntimeContext<E>>,
|
||||
builder_registration_timestamp_override: Option<u64>,
|
||||
validator_registration_batch_size: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
validator_store: None,
|
||||
slot_clock: None,
|
||||
beacon_nodes: None,
|
||||
context: None,
|
||||
builder_registration_timestamp_override: None,
|
||||
validator_registration_batch_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validator_store(mut self, store: Arc<ValidatorStore<T, E>>) -> Self {
|
||||
self.validator_store = Some(store);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn slot_clock(mut self, slot_clock: T) -> Self {
|
||||
self.slot_clock = Some(slot_clock);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn beacon_nodes(mut self, beacon_nodes: Arc<BeaconNodeFallback<T, E>>) -> Self {
|
||||
self.beacon_nodes = Some(beacon_nodes);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn runtime_context(mut self, context: RuntimeContext<E>) -> Self {
|
||||
self.context = Some(context);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn builder_registration_timestamp_override(
|
||||
mut self,
|
||||
builder_registration_timestamp_override: Option<u64>,
|
||||
) -> Self {
|
||||
self.builder_registration_timestamp_override = builder_registration_timestamp_override;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validator_registration_batch_size(
|
||||
mut self,
|
||||
validator_registration_batch_size: usize,
|
||||
) -> Self {
|
||||
self.validator_registration_batch_size = Some(validator_registration_batch_size);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<PreparationService<T, E>, String> {
|
||||
Ok(PreparationService {
|
||||
inner: Arc::new(Inner {
|
||||
validator_store: self
|
||||
.validator_store
|
||||
.ok_or("Cannot build PreparationService without validator_store")?,
|
||||
slot_clock: self
|
||||
.slot_clock
|
||||
.ok_or("Cannot build PreparationService without slot_clock")?,
|
||||
beacon_nodes: self
|
||||
.beacon_nodes
|
||||
.ok_or("Cannot build PreparationService without beacon_nodes")?,
|
||||
context: self
|
||||
.context
|
||||
.ok_or("Cannot build PreparationService without runtime_context")?,
|
||||
builder_registration_timestamp_override: self
|
||||
.builder_registration_timestamp_override,
|
||||
validator_registration_batch_size: self.validator_registration_batch_size.ok_or(
|
||||
"Cannot build PreparationService without validator_registration_batch_size",
|
||||
)?,
|
||||
validator_registration_cache: RwLock::new(HashMap::new()),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to minimise `Arc` usage.
|
||||
pub struct Inner<T, E: EthSpec> {
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
slot_clock: T,
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
|
||||
context: RuntimeContext<E>,
|
||||
builder_registration_timestamp_override: Option<u64>,
|
||||
// Used to track unpublished validator registration changes.
|
||||
validator_registration_cache:
|
||||
RwLock<HashMap<ValidatorRegistrationKey, SignedValidatorRegistrationData>>,
|
||||
validator_registration_batch_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Debug, Clone)]
|
||||
pub struct ValidatorRegistrationKey {
|
||||
pub fee_recipient: Address,
|
||||
pub gas_limit: u64,
|
||||
pub pubkey: PublicKeyBytes,
|
||||
}
|
||||
|
||||
impl From<ValidatorRegistrationData> for ValidatorRegistrationKey {
|
||||
fn from(data: ValidatorRegistrationData) -> Self {
|
||||
let ValidatorRegistrationData {
|
||||
fee_recipient,
|
||||
gas_limit,
|
||||
timestamp: _,
|
||||
pubkey,
|
||||
} = data;
|
||||
Self {
|
||||
fee_recipient,
|
||||
gas_limit,
|
||||
pubkey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to produce proposer preparations for all known validators at the beginning of each epoch.
|
||||
pub struct PreparationService<T, E: EthSpec> {
|
||||
inner: Arc<Inner<T, E>>,
|
||||
}
|
||||
|
||||
impl<T, E: EthSpec> Clone for PreparationService<T, E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E: EthSpec> Deref for PreparationService<T, E> {
|
||||
type Target = Inner<T, E>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> {
|
||||
pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> {
|
||||
self.clone().start_validator_registration_service(spec)?;
|
||||
self.start_proposer_prepare_service(spec)
|
||||
}
|
||||
|
||||
/// Starts the service which periodically produces proposer preparations.
|
||||
pub fn start_proposer_prepare_service(self, spec: &ChainSpec) -> Result<(), String> {
|
||||
let log = self.context.log().clone();
|
||||
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
info!(
|
||||
log,
|
||||
"Proposer preparation service started";
|
||||
);
|
||||
|
||||
let executor = self.context.executor.clone();
|
||||
let spec = spec.clone();
|
||||
|
||||
let interval_fut = async move {
|
||||
loop {
|
||||
if self.should_publish_at_current_slot(&spec) {
|
||||
// Poll the endpoint immediately to ensure fee recipients are received.
|
||||
self.prepare_proposers_and_publish(&spec)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
log,
|
||||
"Error during proposer preparation";
|
||||
"error" => ?e,
|
||||
)
|
||||
})
|
||||
.unwrap_or(());
|
||||
}
|
||||
|
||||
if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() {
|
||||
sleep(duration_to_next_slot).await;
|
||||
} else {
|
||||
error!(log, "Failed to read slot clock");
|
||||
// If we can't read the slot clock, just wait another slot.
|
||||
sleep(slot_duration).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
executor.spawn(interval_fut, "preparation_service");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts the service which periodically sends connected beacon nodes validator registration information.
|
||||
pub fn start_validator_registration_service(self, spec: &ChainSpec) -> Result<(), String> {
|
||||
let log = self.context.log().clone();
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Validator registration service started";
|
||||
);
|
||||
|
||||
let spec = spec.clone();
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
|
||||
let executor = self.context.executor.clone();
|
||||
|
||||
let validator_registration_fut = async move {
|
||||
loop {
|
||||
// Poll the endpoint immediately to ensure fee recipients are received.
|
||||
if let Err(e) = self.register_validators().await {
|
||||
error!(log,"Error during validator registration";"error" => ?e);
|
||||
}
|
||||
|
||||
// Wait one slot if the register validator request fails or if we should not publish at the current slot.
|
||||
if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() {
|
||||
sleep(duration_to_next_slot).await;
|
||||
} else {
|
||||
error!(log, "Failed to read slot clock");
|
||||
// If we can't read the slot clock, just wait another slot.
|
||||
sleep(slot_duration).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
executor.spawn(validator_registration_fut, "validator_registration_service");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return `true` if the current slot is close to or past the Bellatrix fork epoch.
|
||||
///
|
||||
/// This avoids spamming the BN with preparations before the Bellatrix fork epoch, which may
|
||||
/// cause errors if it doesn't support the preparation API.
|
||||
fn should_publish_at_current_slot(&self, spec: &ChainSpec) -> bool {
|
||||
let current_epoch = self
|
||||
.slot_clock
|
||||
.now()
|
||||
.map_or(E::genesis_epoch(), |slot| slot.epoch(E::slots_per_epoch()));
|
||||
spec.bellatrix_fork_epoch.map_or(false, |fork_epoch| {
|
||||
current_epoch + PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS >= fork_epoch
|
||||
})
|
||||
}
|
||||
|
||||
/// Prepare proposer preparations and send to beacon node
|
||||
async fn prepare_proposers_and_publish(&self, spec: &ChainSpec) -> Result<(), String> {
|
||||
let preparation_data = self.collect_preparation_data(spec);
|
||||
if !preparation_data.is_empty() {
|
||||
self.publish_preparation_data(preparation_data).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_preparation_data(&self, spec: &ChainSpec) -> Vec<ProposerPreparationData> {
|
||||
let log = self.context.log();
|
||||
self.collect_proposal_data(|pubkey, proposal_data| {
|
||||
if let Some(fee_recipient) = proposal_data.fee_recipient {
|
||||
Some(ProposerPreparationData {
|
||||
// Ignore fee recipients for keys without indices, they are inactive.
|
||||
validator_index: proposal_data.validator_index?,
|
||||
fee_recipient,
|
||||
})
|
||||
} else {
|
||||
if spec.bellatrix_fork_epoch.is_some() {
|
||||
error!(
|
||||
log,
|
||||
"Validator is missing fee recipient";
|
||||
"msg" => "update validator_definitions.yml",
|
||||
"pubkey" => ?pubkey
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_validator_registration_keys(&self) -> Vec<ValidatorRegistrationKey> {
|
||||
self.collect_proposal_data(|pubkey, proposal_data| {
|
||||
// Ignore fee recipients for keys without indices, they are inactive.
|
||||
proposal_data.validator_index?;
|
||||
|
||||
// We don't log for missing fee recipients here because this will be logged more
|
||||
// frequently in `collect_preparation_data`.
|
||||
proposal_data.fee_recipient.and_then(|fee_recipient| {
|
||||
proposal_data
|
||||
.builder_proposals
|
||||
.then_some(ValidatorRegistrationKey {
|
||||
fee_recipient,
|
||||
gas_limit: proposal_data.gas_limit,
|
||||
pubkey,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_proposal_data<G, U>(&self, map_fn: G) -> Vec<U>
|
||||
where
|
||||
G: Fn(PublicKeyBytes, ProposalData) -> Option<U>,
|
||||
{
|
||||
let all_pubkeys: Vec<_> = self
|
||||
.validator_store
|
||||
.voting_pubkeys(DoppelgangerStatus::ignored);
|
||||
|
||||
all_pubkeys
|
||||
.into_iter()
|
||||
.filter_map(|pubkey| {
|
||||
let proposal_data = self.validator_store.proposal_data(&pubkey)?;
|
||||
map_fn(pubkey, proposal_data)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn publish_preparation_data(
|
||||
&self,
|
||||
preparation_data: Vec<ProposerPreparationData>,
|
||||
) -> Result<(), String> {
|
||||
let log = self.context.log();
|
||||
|
||||
// Post the proposer preparations to the BN.
|
||||
let preparation_data_len = preparation_data.len();
|
||||
let preparation_entries = preparation_data.as_slice();
|
||||
match self
|
||||
.beacon_nodes
|
||||
.request(ApiTopic::Subscriptions, |beacon_node| async move {
|
||||
beacon_node
|
||||
.post_validator_prepare_beacon_proposer(preparation_entries)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => debug!(
|
||||
log,
|
||||
"Published proposer preparation";
|
||||
"count" => preparation_data_len,
|
||||
),
|
||||
Err(e) => error!(
|
||||
log,
|
||||
"Unable to publish proposer preparation to all beacon nodes";
|
||||
"error" => %e,
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register validators with builders, used in the blinded block proposal flow.
|
||||
async fn register_validators(&self) -> Result<(), String> {
|
||||
let registration_keys = self.collect_validator_registration_keys();
|
||||
|
||||
let mut changed_keys = vec![];
|
||||
|
||||
// Need to scope this so the read lock is not held across an await point (I don't know why
|
||||
// but the explicit `drop` is not enough).
|
||||
{
|
||||
let guard = self.validator_registration_cache.read();
|
||||
for key in registration_keys.iter() {
|
||||
if !guard.contains_key(key) {
|
||||
changed_keys.push(key.clone());
|
||||
}
|
||||
}
|
||||
drop(guard);
|
||||
}
|
||||
|
||||
// Check if any have changed or it's been `EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION`.
|
||||
if let Some(slot) = self.slot_clock.now() {
|
||||
if slot % (E::slots_per_epoch() * EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION) == 0 {
|
||||
self.publish_validator_registration_data(registration_keys)
|
||||
.await?;
|
||||
} else if !changed_keys.is_empty() {
|
||||
self.publish_validator_registration_data(changed_keys)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish_validator_registration_data(
|
||||
&self,
|
||||
registration_keys: Vec<ValidatorRegistrationKey>,
|
||||
) -> Result<(), String> {
|
||||
let log = self.context.log();
|
||||
|
||||
let registration_data_len = registration_keys.len();
|
||||
let mut signed = Vec::with_capacity(registration_data_len);
|
||||
|
||||
for key in registration_keys {
|
||||
let cached_registration_opt =
|
||||
self.validator_registration_cache.read().get(&key).cloned();
|
||||
|
||||
let signed_data = if let Some(signed_data) = cached_registration_opt {
|
||||
signed_data
|
||||
} else {
|
||||
let timestamp =
|
||||
if let Some(timestamp) = self.builder_registration_timestamp_override {
|
||||
timestamp
|
||||
} else {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| format!("{e:?}"))?
|
||||
.as_secs()
|
||||
};
|
||||
|
||||
let ValidatorRegistrationKey {
|
||||
fee_recipient,
|
||||
gas_limit,
|
||||
pubkey,
|
||||
} = key.clone();
|
||||
|
||||
let signed_data = match self
|
||||
.validator_store
|
||||
.sign_validator_registration_data(ValidatorRegistrationData {
|
||||
fee_recipient,
|
||||
gas_limit,
|
||||
timestamp,
|
||||
pubkey,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(ValidatorStoreError::UnknownPubkey(pubkey)) => {
|
||||
// A pubkey can be missing when a validator was recently
|
||||
// removed via the API.
|
||||
debug!(
|
||||
log,
|
||||
"Missing pubkey for registration data";
|
||||
"pubkey" => ?pubkey,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
log,
|
||||
"Unable to sign validator registration data";
|
||||
"error" => ?e,
|
||||
"pubkey" => ?pubkey
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
self.validator_registration_cache
|
||||
.write()
|
||||
.insert(key, signed_data.clone());
|
||||
|
||||
signed_data
|
||||
};
|
||||
signed.push(signed_data);
|
||||
}
|
||||
|
||||
if !signed.is_empty() {
|
||||
for batch in signed.chunks(self.validator_registration_batch_size) {
|
||||
match self
|
||||
.beacon_nodes
|
||||
.broadcast(|beacon_node| async move {
|
||||
beacon_node.post_validator_register_validator(batch).await
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => info!(
|
||||
log,
|
||||
"Published validator registrations to the builder network";
|
||||
"count" => batch.len(),
|
||||
),
|
||||
Err(e) => warn!(
|
||||
log,
|
||||
"Unable to publish validator registrations to the builder network";
|
||||
"error" => %e,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper struct, used for passing data from the validator store to services.
|
||||
pub struct ProposalData {
|
||||
pub(crate) validator_index: Option<u64>,
|
||||
pub(crate) fee_recipient: Option<Address>,
|
||||
pub(crate) gas_limit: u64,
|
||||
pub(crate) builder_proposals: bool,
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
//! Provides methods for obtaining validator signatures, including:
|
||||
//!
|
||||
//! - Via a local `Keypair`.
|
||||
//! - Via a remote signer (Web3Signer)
|
||||
|
||||
use crate::http_metrics::metrics;
|
||||
use eth2_keystore::Keystore;
|
||||
use lockfile::Lockfile;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest::{header::ACCEPT, Client};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use task_executor::TaskExecutor;
|
||||
use types::*;
|
||||
use url::Url;
|
||||
use web3signer::{ForkInfo, SigningRequest, SigningResponse};
|
||||
|
||||
pub use web3signer::Web3SignerObject;
|
||||
|
||||
mod web3signer;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
InconsistentDomains {
|
||||
message_type_domain: Domain,
|
||||
domain: Domain,
|
||||
},
|
||||
Web3SignerRequestFailed(String),
|
||||
Web3SignerJsonParsingFailed(String),
|
||||
ShuttingDown,
|
||||
TokioJoin(String),
|
||||
MergeForkNotSupported,
|
||||
GenesisForkVersionRequired,
|
||||
}
|
||||
|
||||
/// Enumerates all messages that can be signed by a validator.
|
||||
pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload<E> = FullPayload<E>> {
|
||||
RandaoReveal(Epoch),
|
||||
BeaconBlock(&'a BeaconBlock<E, Payload>),
|
||||
AttestationData(&'a AttestationData),
|
||||
SignedAggregateAndProof(AggregateAndProofRef<'a, E>),
|
||||
SelectionProof(Slot),
|
||||
SyncSelectionProof(&'a SyncAggregatorSelectionData),
|
||||
SyncCommitteeSignature {
|
||||
beacon_block_root: Hash256,
|
||||
slot: Slot,
|
||||
},
|
||||
SignedContributionAndProof(&'a ContributionAndProof<E>),
|
||||
ValidatorRegistration(&'a ValidatorRegistrationData),
|
||||
VoluntaryExit(&'a VoluntaryExit),
|
||||
}
|
||||
|
||||
impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> SignableMessage<'a, E, Payload> {
|
||||
/// Returns the `SignedRoot` for the contained message.
|
||||
///
|
||||
/// The actual `SignedRoot` trait is not used since it also requires a `TreeHash` impl, which is
|
||||
/// not required here.
|
||||
pub fn signing_root(&self, domain: Hash256) -> Hash256 {
|
||||
match self {
|
||||
SignableMessage::RandaoReveal(epoch) => epoch.signing_root(domain),
|
||||
SignableMessage::BeaconBlock(b) => b.signing_root(domain),
|
||||
SignableMessage::AttestationData(a) => a.signing_root(domain),
|
||||
SignableMessage::SignedAggregateAndProof(a) => a.signing_root(domain),
|
||||
SignableMessage::SelectionProof(slot) => slot.signing_root(domain),
|
||||
SignableMessage::SyncSelectionProof(s) => s.signing_root(domain),
|
||||
SignableMessage::SyncCommitteeSignature {
|
||||
beacon_block_root, ..
|
||||
} => 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A method used by a validator to sign messages.
|
||||
///
|
||||
/// Presently there is only a single variant, however we expect more variants to arise (e.g.,
|
||||
/// remote signing).
|
||||
pub enum SigningMethod {
|
||||
/// A validator that is defined by an EIP-2335 keystore on the local filesystem.
|
||||
LocalKeystore {
|
||||
voting_keystore_path: PathBuf,
|
||||
voting_keystore_lockfile: Mutex<Option<Lockfile>>,
|
||||
voting_keystore: Keystore,
|
||||
voting_keypair: Arc<Keypair>,
|
||||
},
|
||||
/// A validator that defers to a Web3Signer server for signing.
|
||||
///
|
||||
/// See: https://docs.web3signer.consensys.net/en/latest/
|
||||
Web3Signer {
|
||||
signing_url: Url,
|
||||
http_client: Client,
|
||||
voting_public_key: PublicKey,
|
||||
},
|
||||
}
|
||||
|
||||
/// The additional information used to construct a signature. Mostly used for protection from replay
|
||||
/// attacks.
|
||||
pub struct SigningContext {
|
||||
pub domain: Domain,
|
||||
pub epoch: Epoch,
|
||||
pub fork: Fork,
|
||||
pub genesis_validators_root: Hash256,
|
||||
}
|
||||
|
||||
impl SigningContext {
|
||||
/// Returns the `Hash256` to be mixed-in with the signature.
|
||||
pub fn domain_hash(&self, spec: &ChainSpec) -> Hash256 {
|
||||
spec.get_domain(
|
||||
self.epoch,
|
||||
self.domain,
|
||||
&self.fork,
|
||||
self.genesis_validators_root,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SigningMethod {
|
||||
/// Return whether this signing method requires local slashing protection.
|
||||
pub fn requires_local_slashing_protection(
|
||||
&self,
|
||||
enable_web3signer_slashing_protection: bool,
|
||||
) -> bool {
|
||||
match self {
|
||||
// Slashing protection is ALWAYS required for local keys. DO NOT TURN THIS OFF.
|
||||
SigningMethod::LocalKeystore { .. } => true,
|
||||
// Slashing protection is only required for remote signer keys when the configuration
|
||||
// dictates that it is desired.
|
||||
SigningMethod::Web3Signer { .. } => enable_web3signer_slashing_protection,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the signature of `signable_message`, with respect to the `signing_context`.
|
||||
pub async fn get_signature<E: EthSpec, Payload: AbstractExecPayload<E>>(
|
||||
&self,
|
||||
signable_message: SignableMessage<'_, E, Payload>,
|
||||
signing_context: SigningContext,
|
||||
spec: &ChainSpec,
|
||||
executor: &TaskExecutor,
|
||||
) -> Result<Signature, Error> {
|
||||
let domain_hash = signing_context.domain_hash(spec);
|
||||
let SigningContext {
|
||||
fork,
|
||||
genesis_validators_root,
|
||||
..
|
||||
} = signing_context;
|
||||
|
||||
let signing_root = signable_message.signing_root(domain_hash);
|
||||
|
||||
let fork_info = Some(ForkInfo {
|
||||
fork,
|
||||
genesis_validators_root,
|
||||
});
|
||||
|
||||
self.get_signature_from_root(signable_message, signing_root, executor, fork_info)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_signature_from_root<E: EthSpec, Payload: AbstractExecPayload<E>>(
|
||||
&self,
|
||||
signable_message: SignableMessage<'_, E, Payload>,
|
||||
signing_root: Hash256,
|
||||
executor: &TaskExecutor,
|
||||
fork_info: Option<ForkInfo>,
|
||||
) -> Result<Signature, Error> {
|
||||
match self {
|
||||
SigningMethod::LocalKeystore { voting_keypair, .. } => {
|
||||
let _timer =
|
||||
metrics::start_timer_vec(&metrics::SIGNING_TIMES, &[metrics::LOCAL_KEYSTORE]);
|
||||
|
||||
let voting_keypair = voting_keypair.clone();
|
||||
// Spawn a blocking task to produce the signature. This avoids blocking the core
|
||||
// tokio executor.
|
||||
let signature = executor
|
||||
.spawn_blocking_handle(
|
||||
move || voting_keypair.sk.sign(signing_root),
|
||||
"local_keystore_signer",
|
||||
)
|
||||
.ok_or(Error::ShuttingDown)?
|
||||
.await
|
||||
.map_err(|e| Error::TokioJoin(e.to_string()))?;
|
||||
Ok(signature)
|
||||
}
|
||||
SigningMethod::Web3Signer {
|
||||
signing_url,
|
||||
http_client,
|
||||
..
|
||||
} => {
|
||||
let _timer =
|
||||
metrics::start_timer_vec(&metrics::SIGNING_TIMES, &[metrics::WEB3SIGNER]);
|
||||
|
||||
// Map the message into a Web3Signer type.
|
||||
let object = match signable_message {
|
||||
SignableMessage::RandaoReveal(epoch) => {
|
||||
Web3SignerObject::RandaoReveal { epoch }
|
||||
}
|
||||
SignableMessage::BeaconBlock(block) => Web3SignerObject::beacon_block(block)?,
|
||||
SignableMessage::AttestationData(a) => Web3SignerObject::Attestation(a),
|
||||
SignableMessage::SignedAggregateAndProof(a) => {
|
||||
Web3SignerObject::AggregateAndProof(a)
|
||||
}
|
||||
SignableMessage::SelectionProof(slot) => {
|
||||
Web3SignerObject::AggregationSlot { slot }
|
||||
}
|
||||
SignableMessage::SyncSelectionProof(s) => {
|
||||
Web3SignerObject::SyncAggregatorSelectionData(s)
|
||||
}
|
||||
SignableMessage::SyncCommitteeSignature {
|
||||
beacon_block_root,
|
||||
slot,
|
||||
} => Web3SignerObject::SyncCommitteeMessage {
|
||||
beacon_block_root,
|
||||
slot,
|
||||
},
|
||||
SignableMessage::SignedContributionAndProof(c) => {
|
||||
Web3SignerObject::ContributionAndProof(c)
|
||||
}
|
||||
SignableMessage::ValidatorRegistration(v) => {
|
||||
Web3SignerObject::ValidatorRegistration(v)
|
||||
}
|
||||
SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e),
|
||||
};
|
||||
|
||||
// Determine the Web3Signer message type.
|
||||
let message_type = object.message_type();
|
||||
|
||||
if matches!(
|
||||
object,
|
||||
Web3SignerObject::Deposit { .. } | Web3SignerObject::ValidatorRegistration(_)
|
||||
) && fork_info.is_some()
|
||||
{
|
||||
return Err(Error::GenesisForkVersionRequired);
|
||||
}
|
||||
|
||||
let request = SigningRequest {
|
||||
message_type,
|
||||
fork_info,
|
||||
signing_root,
|
||||
object,
|
||||
};
|
||||
|
||||
// Request a signature from the Web3Signer instance via HTTP(S).
|
||||
let response: SigningResponse = http_client
|
||||
.post(signing_url.clone())
|
||||
.header(ACCEPT, "application/json")
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| Error::Web3SignerRequestFailed(e.to_string()))?
|
||||
.error_for_status()
|
||||
.map_err(|e| Error::Web3SignerRequestFailed(e.to_string()))?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| Error::Web3SignerJsonParsingFailed(e.to_string()))?;
|
||||
|
||||
Ok(response.signature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
//! Contains the types required to make JSON requests to Web3Signer servers.
|
||||
|
||||
use super::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum MessageType {
|
||||
AggregationSlot,
|
||||
AggregateAndProof,
|
||||
Attestation,
|
||||
BlockV2,
|
||||
Deposit,
|
||||
RandaoReveal,
|
||||
VoluntaryExit,
|
||||
SyncCommitteeMessage,
|
||||
SyncCommitteeSelectionProof,
|
||||
SyncCommitteeContributionAndProof,
|
||||
ValidatorRegistration,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ForkName {
|
||||
Phase0,
|
||||
Altair,
|
||||
Bellatrix,
|
||||
Capella,
|
||||
Deneb,
|
||||
Electra,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct ForkInfo {
|
||||
pub fork: Fork,
|
||||
pub genesis_validators_root: Hash256,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
#[serde(bound = "E: EthSpec", rename_all = "snake_case")]
|
||||
pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload<E>> {
|
||||
AggregationSlot {
|
||||
slot: Slot,
|
||||
},
|
||||
AggregateAndProof(AggregateAndProofRef<'a, E>),
|
||||
Attestation(&'a AttestationData),
|
||||
BeaconBlock {
|
||||
version: ForkName,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
block: Option<&'a BeaconBlock<E, Payload>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
block_header: Option<BeaconBlockHeader>,
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
Deposit {
|
||||
pubkey: PublicKeyBytes,
|
||||
withdrawal_credentials: Hash256,
|
||||
#[serde(with = "serde_utils::quoted_u64")]
|
||||
amount: u64,
|
||||
#[serde(with = "serde_utils::bytes_4_hex")]
|
||||
genesis_fork_version: [u8; 4],
|
||||
},
|
||||
RandaoReveal {
|
||||
epoch: Epoch,
|
||||
},
|
||||
VoluntaryExit(&'a VoluntaryExit),
|
||||
SyncCommitteeMessage {
|
||||
beacon_block_root: Hash256,
|
||||
slot: Slot,
|
||||
},
|
||||
SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData),
|
||||
ContributionAndProof(&'a ContributionAndProof<E>),
|
||||
ValidatorRegistration(&'a ValidatorRegistrationData),
|
||||
}
|
||||
|
||||
impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> Web3SignerObject<'a, E, Payload> {
|
||||
pub fn beacon_block(block: &'a BeaconBlock<E, Payload>) -> Result<Self, Error> {
|
||||
match block {
|
||||
BeaconBlock::Base(_) => Ok(Web3SignerObject::BeaconBlock {
|
||||
version: ForkName::Phase0,
|
||||
block: Some(block),
|
||||
block_header: None,
|
||||
}),
|
||||
BeaconBlock::Altair(_) => Ok(Web3SignerObject::BeaconBlock {
|
||||
version: ForkName::Altair,
|
||||
block: Some(block),
|
||||
block_header: None,
|
||||
}),
|
||||
BeaconBlock::Bellatrix(_) => Ok(Web3SignerObject::BeaconBlock {
|
||||
version: ForkName::Bellatrix,
|
||||
block: None,
|
||||
block_header: Some(block.block_header()),
|
||||
}),
|
||||
BeaconBlock::Capella(_) => Ok(Web3SignerObject::BeaconBlock {
|
||||
version: ForkName::Capella,
|
||||
block: None,
|
||||
block_header: Some(block.block_header()),
|
||||
}),
|
||||
BeaconBlock::Deneb(_) => Ok(Web3SignerObject::BeaconBlock {
|
||||
version: ForkName::Deneb,
|
||||
block: None,
|
||||
block_header: Some(block.block_header()),
|
||||
}),
|
||||
BeaconBlock::Electra(_) => Ok(Web3SignerObject::BeaconBlock {
|
||||
version: ForkName::Electra,
|
||||
block: None,
|
||||
block_header: Some(block.block_header()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_type(&self) -> MessageType {
|
||||
match self {
|
||||
Web3SignerObject::AggregationSlot { .. } => MessageType::AggregationSlot,
|
||||
Web3SignerObject::AggregateAndProof(_) => MessageType::AggregateAndProof,
|
||||
Web3SignerObject::Attestation(_) => MessageType::Attestation,
|
||||
Web3SignerObject::BeaconBlock { .. } => MessageType::BlockV2,
|
||||
Web3SignerObject::Deposit { .. } => MessageType::Deposit,
|
||||
Web3SignerObject::RandaoReveal { .. } => MessageType::RandaoReveal,
|
||||
Web3SignerObject::VoluntaryExit(_) => MessageType::VoluntaryExit,
|
||||
Web3SignerObject::SyncCommitteeMessage { .. } => MessageType::SyncCommitteeMessage,
|
||||
Web3SignerObject::SyncAggregatorSelectionData(_) => {
|
||||
MessageType::SyncCommitteeSelectionProof
|
||||
}
|
||||
Web3SignerObject::ContributionAndProof(_) => {
|
||||
MessageType::SyncCommitteeContributionAndProof
|
||||
}
|
||||
Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
#[serde(bound = "E: EthSpec")]
|
||||
pub struct SigningRequest<'a, E: EthSpec, Payload: AbstractExecPayload<E>> {
|
||||
#[serde(rename = "type")]
|
||||
pub message_type: MessageType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fork_info: Option<ForkInfo>,
|
||||
#[serde(rename = "signingRoot")]
|
||||
pub signing_root: Hash256,
|
||||
#[serde(flatten)]
|
||||
pub object: Web3SignerObject<'a, E, Payload>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize)]
|
||||
pub struct SigningResponse {
|
||||
pub signature: Signature,
|
||||
}
|
||||
@@ -1,626 +0,0 @@
|
||||
use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback};
|
||||
use crate::{
|
||||
duties_service::DutiesService,
|
||||
validator_store::{Error as ValidatorStoreError, ValidatorStore},
|
||||
};
|
||||
use environment::RuntimeContext;
|
||||
use eth2::types::BlockId;
|
||||
use futures::future::join_all;
|
||||
use futures::future::FutureExt;
|
||||
use slog::{crit, debug, error, info, trace, warn};
|
||||
use slot_clock::SlotClock;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{sleep, sleep_until, Duration, Instant};
|
||||
use types::{
|
||||
ChainSpec, EthSpec, Hash256, PublicKeyBytes, Slot, SyncCommitteeSubscription,
|
||||
SyncContributionData, SyncDuty, SyncSelectionProof, SyncSubnetId,
|
||||
};
|
||||
|
||||
pub const SUBSCRIPTION_LOOKAHEAD_EPOCHS: u64 = 4;
|
||||
|
||||
pub struct SyncCommitteeService<T: SlotClock + 'static, E: EthSpec> {
|
||||
inner: Arc<Inner<T, E>>,
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> Clone for SyncCommitteeService<T, E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> Deref for SyncCommitteeService<T, E> {
|
||||
type Target = Inner<T, E>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner.deref()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Inner<T: SlotClock + 'static, E: EthSpec> {
|
||||
duties_service: Arc<DutiesService<T, E>>,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
slot_clock: T,
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
|
||||
context: RuntimeContext<E>,
|
||||
/// Boolean to track whether the service has posted subscriptions to the BN at least once.
|
||||
///
|
||||
/// This acts as a latch that fires once upon start-up, and then never again.
|
||||
first_subscription_done: AtomicBool,
|
||||
}
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> SyncCommitteeService<T, E> {
|
||||
pub fn new(
|
||||
duties_service: Arc<DutiesService<T, E>>,
|
||||
validator_store: Arc<ValidatorStore<T, E>>,
|
||||
slot_clock: T,
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
|
||||
context: RuntimeContext<E>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Inner {
|
||||
duties_service,
|
||||
validator_store,
|
||||
slot_clock,
|
||||
beacon_nodes,
|
||||
context,
|
||||
first_subscription_done: AtomicBool::new(false),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the Altair fork has been activated and therefore sync duties should be performed.
|
||||
///
|
||||
/// Slot clock errors are mapped to `false`.
|
||||
fn altair_fork_activated(&self) -> bool {
|
||||
self.duties_service
|
||||
.spec
|
||||
.altair_fork_epoch
|
||||
.and_then(|fork_epoch| {
|
||||
let current_epoch = self.slot_clock.now()?.epoch(E::slots_per_epoch());
|
||||
Some(current_epoch >= fork_epoch)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> {
|
||||
let log = self.context.log().clone();
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
let duration_to_next_slot = self
|
||||
.slot_clock
|
||||
.duration_to_next_slot()
|
||||
.ok_or("Unable to determine duration to next slot")?;
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Sync committee service started";
|
||||
"next_update_millis" => duration_to_next_slot.as_millis()
|
||||
);
|
||||
|
||||
let executor = self.context.executor.clone();
|
||||
|
||||
let interval_fut = async move {
|
||||
loop {
|
||||
if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() {
|
||||
// Wait for contribution broadcast interval 1/3 of the way through the slot.
|
||||
let log = self.context.log();
|
||||
sleep(duration_to_next_slot + slot_duration / 3).await;
|
||||
|
||||
// Do nothing if the Altair fork has not yet occurred.
|
||||
if !self.altair_fork_activated() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = self.spawn_contribution_tasks(slot_duration).await {
|
||||
crit!(
|
||||
log,
|
||||
"Failed to spawn sync contribution tasks";
|
||||
"error" => e
|
||||
)
|
||||
} else {
|
||||
trace!(
|
||||
log,
|
||||
"Spawned sync contribution tasks";
|
||||
)
|
||||
}
|
||||
|
||||
// Do subscriptions for future slots/epochs.
|
||||
self.spawn_subscription_tasks();
|
||||
} else {
|
||||
error!(log, "Failed to read slot clock");
|
||||
// If we can't read the slot clock, just wait another slot.
|
||||
sleep(slot_duration).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
executor.spawn(interval_fut, "sync_committee_service");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn spawn_contribution_tasks(&self, slot_duration: Duration) -> Result<(), String> {
|
||||
let log = self.context.log().clone();
|
||||
let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?;
|
||||
let duration_to_next_slot = self
|
||||
.slot_clock
|
||||
.duration_to_next_slot()
|
||||
.ok_or("Unable to determine duration to next slot")?;
|
||||
|
||||
// If a validator needs to publish a sync aggregate, they must do so at 2/3
|
||||
// through the slot. This delay triggers at this time
|
||||
let aggregate_production_instant = Instant::now()
|
||||
+ duration_to_next_slot
|
||||
.checked_sub(slot_duration / 3)
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
let Some(slot_duties) = self
|
||||
.duties_service
|
||||
.sync_duties
|
||||
.get_duties_for_slot(slot, &self.duties_service.spec)
|
||||
else {
|
||||
debug!(log, "No duties known for slot {}", slot);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if slot_duties.duties.is_empty() {
|
||||
debug!(
|
||||
log,
|
||||
"No local validators in current sync committee";
|
||||
"slot" => slot,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Fetch `block_root` with non optimistic execution for `SyncCommitteeContribution`.
|
||||
let response = self
|
||||
.beacon_nodes
|
||||
.first_success(
|
||||
|beacon_node| async move {
|
||||
match beacon_node.get_beacon_blocks_root(BlockId::Head).await {
|
||||
Ok(Some(block)) if block.execution_optimistic == Some(false) => {
|
||||
Ok(block)
|
||||
}
|
||||
Ok(Some(_)) => {
|
||||
Err(format!("To sign sync committee messages for slot {slot} a non-optimistic head block is required"))
|
||||
}
|
||||
Ok(None) => Err(format!("No block root found for slot {}", slot)),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let block_root = match response {
|
||||
Ok(block) => block.data.root,
|
||||
Err(errs) => {
|
||||
warn!(
|
||||
log,
|
||||
"Refusing to sign sync committee messages for an optimistic head block or \
|
||||
a block head with unknown optimistic status";
|
||||
"errors" => errs.to_string(),
|
||||
"slot" => slot,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Spawn one task to publish all of the sync committee signatures.
|
||||
let validator_duties = slot_duties.duties;
|
||||
let service = self.clone();
|
||||
self.inner.context.executor.spawn(
|
||||
async move {
|
||||
service
|
||||
.publish_sync_committee_signatures(slot, block_root, validator_duties)
|
||||
.map(|_| ())
|
||||
.await
|
||||
},
|
||||
"sync_committee_signature_publish",
|
||||
);
|
||||
|
||||
let aggregators = slot_duties.aggregators;
|
||||
let service = self.clone();
|
||||
self.inner.context.executor.spawn(
|
||||
async move {
|
||||
service
|
||||
.publish_sync_committee_aggregates(
|
||||
slot,
|
||||
block_root,
|
||||
aggregators,
|
||||
aggregate_production_instant,
|
||||
)
|
||||
.map(|_| ())
|
||||
.await
|
||||
},
|
||||
"sync_committee_aggregate_publish",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish sync committee signatures.
|
||||
async fn publish_sync_committee_signatures(
|
||||
&self,
|
||||
slot: Slot,
|
||||
beacon_block_root: Hash256,
|
||||
validator_duties: Vec<SyncDuty>,
|
||||
) -> Result<(), ()> {
|
||||
let log = self.context.log();
|
||||
|
||||
// Create futures to produce sync committee signatures.
|
||||
let signature_futures = validator_duties.iter().map(|duty| async move {
|
||||
match self
|
||||
.validator_store
|
||||
.produce_sync_committee_signature(
|
||||
slot,
|
||||
beacon_block_root,
|
||||
duty.validator_index,
|
||||
&duty.pubkey,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(signature) => Some(signature),
|
||||
Err(ValidatorStoreError::UnknownPubkey(pubkey)) => {
|
||||
// A pubkey can be missing when a validator was recently
|
||||
// removed via the API.
|
||||
debug!(
|
||||
log,
|
||||
"Missing pubkey for sync committee signature";
|
||||
"pubkey" => ?pubkey,
|
||||
"validator_index" => duty.validator_index,
|
||||
"slot" => slot,
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
log,
|
||||
"Failed to sign sync committee signature";
|
||||
"validator_index" => duty.validator_index,
|
||||
"slot" => slot,
|
||||
"error" => ?e,
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let committee_signatures = &join_all(signature_futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.beacon_nodes
|
||||
.request(ApiTopic::SyncCommittee, |beacon_node| async move {
|
||||
beacon_node
|
||||
.post_beacon_pool_sync_committee_signatures(committee_signatures)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
log,
|
||||
"Unable to publish sync committee messages";
|
||||
"slot" => slot,
|
||||
"error" => %e,
|
||||
);
|
||||
})?;
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Successfully published sync committee messages";
|
||||
"count" => committee_signatures.len(),
|
||||
"head_block" => ?beacon_block_root,
|
||||
"slot" => slot,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish_sync_committee_aggregates(
|
||||
&self,
|
||||
slot: Slot,
|
||||
beacon_block_root: Hash256,
|
||||
aggregators: HashMap<SyncSubnetId, Vec<(u64, PublicKeyBytes, SyncSelectionProof)>>,
|
||||
aggregate_instant: Instant,
|
||||
) {
|
||||
for (subnet_id, subnet_aggregators) in aggregators {
|
||||
let service = self.clone();
|
||||
self.inner.context.executor.spawn(
|
||||
async move {
|
||||
service
|
||||
.publish_sync_committee_aggregate_for_subnet(
|
||||
slot,
|
||||
beacon_block_root,
|
||||
subnet_id,
|
||||
subnet_aggregators,
|
||||
aggregate_instant,
|
||||
)
|
||||
.map(|_| ())
|
||||
.await
|
||||
},
|
||||
"sync_committee_aggregate_publish_subnet",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_sync_committee_aggregate_for_subnet(
|
||||
&self,
|
||||
slot: Slot,
|
||||
beacon_block_root: Hash256,
|
||||
subnet_id: SyncSubnetId,
|
||||
subnet_aggregators: Vec<(u64, PublicKeyBytes, SyncSelectionProof)>,
|
||||
aggregate_instant: Instant,
|
||||
) -> Result<(), ()> {
|
||||
sleep_until(aggregate_instant).await;
|
||||
|
||||
let log = self.context.log();
|
||||
|
||||
let contribution = &self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let sync_contribution_data = SyncContributionData {
|
||||
slot,
|
||||
beacon_block_root,
|
||||
subcommittee_index: subnet_id.into(),
|
||||
};
|
||||
|
||||
beacon_node
|
||||
.get_validator_sync_committee_contribution::<E>(&sync_contribution_data)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crit!(
|
||||
log,
|
||||
"Failed to produce sync contribution";
|
||||
"slot" => slot,
|
||||
"beacon_block_root" => ?beacon_block_root,
|
||||
"error" => %e,
|
||||
)
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
crit!(
|
||||
log,
|
||||
"No aggregate contribution found";
|
||||
"slot" => slot,
|
||||
"beacon_block_root" => ?beacon_block_root,
|
||||
);
|
||||
})?
|
||||
.data;
|
||||
|
||||
// Create futures to produce signed contributions.
|
||||
let signature_futures = subnet_aggregators.into_iter().map(
|
||||
|(aggregator_index, aggregator_pk, selection_proof)| async move {
|
||||
match self
|
||||
.validator_store
|
||||
.produce_signed_contribution_and_proof(
|
||||
aggregator_index,
|
||||
aggregator_pk,
|
||||
contribution.clone(),
|
||||
selection_proof,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(signed_contribution) => Some(signed_contribution),
|
||||
Err(ValidatorStoreError::UnknownPubkey(pubkey)) => {
|
||||
// A pubkey can be missing when a validator was recently
|
||||
// removed via the API.
|
||||
debug!(
|
||||
log,
|
||||
"Missing pubkey for sync contribution";
|
||||
"pubkey" => ?pubkey,
|
||||
"slot" => slot,
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
log,
|
||||
"Unable to sign sync committee contribution";
|
||||
"slot" => slot,
|
||||
"error" => ?e,
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let signed_contributions = &join_all(signature_futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Publish to the beacon node.
|
||||
self.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
beacon_node
|
||||
.post_validator_contribution_and_proofs(signed_contributions)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
log,
|
||||
"Unable to publish signed contributions and proofs";
|
||||
"slot" => slot,
|
||||
"error" => %e,
|
||||
);
|
||||
})?;
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Successfully published sync contributions";
|
||||
"subnet" => %subnet_id,
|
||||
"beacon_block_root" => %beacon_block_root,
|
||||
"num_signers" => contribution.aggregation_bits.num_set_bits(),
|
||||
"slot" => slot,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_subscription_tasks(&self) {
|
||||
let service = self.clone();
|
||||
let log = self.context.log().clone();
|
||||
self.inner.context.executor.spawn(
|
||||
async move {
|
||||
service.publish_subscriptions().await.unwrap_or_else(|e| {
|
||||
error!(
|
||||
log,
|
||||
"Error publishing subscriptions";
|
||||
"error" => ?e,
|
||||
)
|
||||
});
|
||||
},
|
||||
"sync_committee_subscription_publish",
|
||||
);
|
||||
}
|
||||
|
||||
async fn publish_subscriptions(self) -> Result<(), String> {
|
||||
let log = self.context.log().clone();
|
||||
let spec = &self.duties_service.spec;
|
||||
let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?;
|
||||
|
||||
let mut duty_slots = vec![];
|
||||
let mut all_succeeded = true;
|
||||
|
||||
// At the start of every epoch during the current period, re-post the subscriptions
|
||||
// to the beacon node. This covers the case where the BN has forgotten the subscriptions
|
||||
// due to a restart, or where the VC has switched to a fallback BN.
|
||||
let current_period = sync_period_of_slot::<E>(slot, spec)?;
|
||||
|
||||
if !self.first_subscription_done.load(Ordering::Relaxed)
|
||||
|| slot.as_u64() % E::slots_per_epoch() == 0
|
||||
{
|
||||
duty_slots.push((slot, current_period));
|
||||
}
|
||||
|
||||
// Near the end of the current period, push subscriptions for the next period to the
|
||||
// beacon node. We aggressively push every slot in the lead-up, as this is the main way
|
||||
// that we want to ensure that the BN is subscribed (well in advance).
|
||||
let lookahead_slot = slot + SUBSCRIPTION_LOOKAHEAD_EPOCHS * E::slots_per_epoch();
|
||||
|
||||
let lookahead_period = sync_period_of_slot::<E>(lookahead_slot, spec)?;
|
||||
|
||||
if lookahead_period > current_period {
|
||||
duty_slots.push((lookahead_slot, lookahead_period));
|
||||
}
|
||||
|
||||
if duty_slots.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Collect subscriptions.
|
||||
let mut subscriptions = vec![];
|
||||
|
||||
for (duty_slot, sync_committee_period) in duty_slots {
|
||||
debug!(
|
||||
log,
|
||||
"Fetching subscription duties";
|
||||
"duty_slot" => duty_slot,
|
||||
"current_slot" => slot,
|
||||
);
|
||||
match self
|
||||
.duties_service
|
||||
.sync_duties
|
||||
.get_duties_for_slot(duty_slot, spec)
|
||||
{
|
||||
Some(duties) => subscriptions.extend(subscriptions_from_sync_duties(
|
||||
duties.duties,
|
||||
sync_committee_period,
|
||||
spec,
|
||||
)),
|
||||
None => {
|
||||
debug!(
|
||||
log,
|
||||
"No duties for subscription";
|
||||
"slot" => duty_slot,
|
||||
);
|
||||
all_succeeded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subscriptions.is_empty() {
|
||||
debug!(
|
||||
log,
|
||||
"No sync subscriptions to send";
|
||||
"slot" => slot,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Post subscriptions to BN.
|
||||
debug!(
|
||||
log,
|
||||
"Posting sync subscriptions to BN";
|
||||
"count" => subscriptions.len(),
|
||||
);
|
||||
let subscriptions_slice = &subscriptions;
|
||||
|
||||
for subscription in subscriptions_slice {
|
||||
debug!(
|
||||
log,
|
||||
"Subscription";
|
||||
"validator_index" => subscription.validator_index,
|
||||
"validator_sync_committee_indices" => ?subscription.sync_committee_indices,
|
||||
"until_epoch" => subscription.until_epoch,
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = self
|
||||
.beacon_nodes
|
||||
.request(ApiTopic::Subscriptions, |beacon_node| async move {
|
||||
beacon_node
|
||||
.post_validator_sync_committee_subscriptions(subscriptions_slice)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
log,
|
||||
"Unable to post sync committee subscriptions";
|
||||
"slot" => slot,
|
||||
"error" => %e,
|
||||
);
|
||||
all_succeeded = false;
|
||||
}
|
||||
|
||||
// Disable first-subscription latch once all duties have succeeded once.
|
||||
if all_succeeded {
|
||||
self.first_subscription_done.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_period_of_slot<E: EthSpec>(slot: Slot, spec: &ChainSpec) -> Result<u64, String> {
|
||||
slot.epoch(E::slots_per_epoch())
|
||||
.sync_committee_period(spec)
|
||||
.map_err(|e| format!("Error computing sync period: {:?}", e))
|
||||
}
|
||||
|
||||
fn subscriptions_from_sync_duties(
|
||||
duties: Vec<SyncDuty>,
|
||||
sync_committee_period: u64,
|
||||
spec: &ChainSpec,
|
||||
) -> impl Iterator<Item = SyncCommitteeSubscription> {
|
||||
let until_epoch = spec.epochs_per_sync_committee_period * (sync_committee_period + 1);
|
||||
duties
|
||||
.into_iter()
|
||||
.map(move |duty| SyncCommitteeSubscription {
|
||||
validator_index: duty.validator_index,
|
||||
sync_committee_indices: duty.validator_sync_committee_indices,
|
||||
until_epoch,
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user