mirror of
https://github.com/sigp/lighthouse.git
synced 2026-06-15 17:58:23 +00:00
resolve merge conflicts
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
use crate::duties_service::{DutiesService, DutyAndProof};
|
||||
use beacon_node_fallback::{ApiTopic, BeaconNodeFallback};
|
||||
use futures::future::join_all;
|
||||
use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, beacon_head_monitor::HeadEvent};
|
||||
use futures::StreamExt;
|
||||
use logging::crit;
|
||||
use slot_clock::SlotClock;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use task_executor::TaskExecutor;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{Duration, Instant, sleep, sleep_until};
|
||||
use tracing::{Instrument, Span, debug, error, info, info_span, instrument, trace, warn};
|
||||
use tracing::{Instrument, debug, error, info, info_span, instrument, warn};
|
||||
use tree_hash::TreeHash;
|
||||
use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot};
|
||||
use validator_store::{Error as ValidatorStoreError, ValidatorStore};
|
||||
use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Hash256, Slot};
|
||||
use validator_store::{AggregateToSign, AttestationToSign, ValidatorStore};
|
||||
|
||||
/// Builds an `AttestationService`.
|
||||
#[derive(Default)]
|
||||
@@ -22,6 +24,7 @@ pub struct AttestationServiceBuilder<S: ValidatorStore, T: SlotClock + 'static>
|
||||
beacon_nodes: Option<Arc<BeaconNodeFallback<T>>>,
|
||||
executor: Option<TaskExecutor>,
|
||||
chain_spec: Option<Arc<ChainSpec>>,
|
||||
head_monitor_rx: Option<Mutex<mpsc::Receiver<HeadEvent>>>,
|
||||
disable: bool,
|
||||
}
|
||||
|
||||
@@ -34,6 +37,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationServiceBuil
|
||||
beacon_nodes: None,
|
||||
executor: None,
|
||||
chain_spec: None,
|
||||
head_monitor_rx: None,
|
||||
disable: false,
|
||||
}
|
||||
}
|
||||
@@ -73,6 +77,13 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationServiceBuil
|
||||
self
|
||||
}
|
||||
|
||||
pub fn head_monitor_rx(
|
||||
mut self,
|
||||
head_monitor_rx: Option<Mutex<mpsc::Receiver<HeadEvent>>>,
|
||||
) -> Self {
|
||||
self.head_monitor_rx = head_monitor_rx;
|
||||
self
|
||||
}
|
||||
pub fn build(self) -> Result<AttestationService<S, T>, String> {
|
||||
Ok(AttestationService {
|
||||
inner: Arc::new(Inner {
|
||||
@@ -94,7 +105,9 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationServiceBuil
|
||||
chain_spec: self
|
||||
.chain_spec
|
||||
.ok_or("Cannot build AttestationService without chain_spec")?,
|
||||
head_monitor_rx: self.head_monitor_rx,
|
||||
disable: self.disable,
|
||||
latest_attested_slot: Mutex::new(Slot::default()),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -108,10 +121,13 @@ pub struct Inner<S, T> {
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T>>,
|
||||
executor: TaskExecutor,
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
head_monitor_rx: Option<Mutex<mpsc::Receiver<HeadEvent>>>,
|
||||
disable: bool,
|
||||
latest_attested_slot: Mutex<Slot>,
|
||||
}
|
||||
|
||||
/// Attempts to produce attestations for all known validators 1/3rd of the way through each slot.
|
||||
/// Attempts to produce attestations for all known validators 1/3rd of the way through each slot
|
||||
/// or when a head event is received from the BNs.
|
||||
///
|
||||
/// 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
|
||||
@@ -144,7 +160,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
let slot_duration = spec.get_slot_duration();
|
||||
let duration_to_next_slot = self
|
||||
.slot_clock
|
||||
.duration_to_next_slot()
|
||||
@@ -157,21 +173,46 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
|
||||
let executor = self.executor.clone();
|
||||
|
||||
let unaggregated_attestation_due = self.chain_spec.get_unaggregated_attestation_due();
|
||||
|
||||
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;
|
||||
|
||||
if let Err(e) = self.spawn_attestation_tasks(slot_duration) {
|
||||
crit!(error = e, "Failed to spawn attestation tasks")
|
||||
} else {
|
||||
trace!("Spawned attestation tasks");
|
||||
}
|
||||
} else {
|
||||
let Some(duration) = self.slot_clock.duration_to_next_slot() else {
|
||||
error!("Failed to read slot clock");
|
||||
// If we can't read the slot clock, just wait another slot.
|
||||
sleep(slot_duration).await;
|
||||
continue;
|
||||
};
|
||||
|
||||
let beacon_node_data = if self.head_monitor_rx.is_some() {
|
||||
tokio::select! {
|
||||
_ = sleep(duration + unaggregated_attestation_due) => None,
|
||||
event = self.poll_for_head_events() =>
|
||||
event.map(|event| (event.beacon_node_index, event.beacon_block_root)),
|
||||
}
|
||||
} else {
|
||||
sleep(duration + unaggregated_attestation_due).await;
|
||||
None
|
||||
};
|
||||
|
||||
let Some(current_slot) = self.slot_clock.now() else {
|
||||
error!("Failed to read slot clock after trigger");
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut last_slot = self.latest_attested_slot.lock().await;
|
||||
|
||||
if current_slot <= *last_slot {
|
||||
debug!(%current_slot, "Attestation already initiated for the slot");
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.spawn_attestation_tasks(beacon_node_data).await {
|
||||
Ok(_) => {
|
||||
*last_slot = current_slot;
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(error = e, "Failed to spawn attestation tasks")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -180,15 +221,38 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn poll_for_head_events(&self) -> Option<HeadEvent> {
|
||||
let Some(receiver) = &self.head_monitor_rx else {
|
||||
return None;
|
||||
};
|
||||
let mut receiver = receiver.lock().await;
|
||||
loop {
|
||||
match receiver.recv().await {
|
||||
Some(head_event) => {
|
||||
// Only return head events for the current slot - this ensures the
|
||||
// block for this slot has been produced before triggering attestation
|
||||
let current_slot = self.slot_clock.now()?;
|
||||
if head_event.slot == current_slot {
|
||||
return Some(head_event);
|
||||
}
|
||||
// Head event is for a previous slot, keep waiting
|
||||
}
|
||||
None => {
|
||||
warn!("Head monitor channel closed unexpectedly");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn only one new task for attestation post-Electra
|
||||
/// For each required aggregates, spawn a new task that downloads, signs and uploads the
|
||||
/// aggregates to the beacon node.
|
||||
fn spawn_attestation_tasks(&self, slot_duration: Duration) -> Result<(), String> {
|
||||
async fn spawn_attestation_tasks(
|
||||
&self,
|
||||
beacon_node_data: Option<(usize, Hash256)>,
|
||||
) -> 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")?;
|
||||
|
||||
// Create and publish an `Attestation` for all validators only once
|
||||
// as the committee_index is not included in AttestationData post-Electra
|
||||
@@ -199,29 +263,89 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!(
|
||||
%slot,
|
||||
from_head_monitor = beacon_node_data.is_some(),
|
||||
"Starting attestation production"
|
||||
);
|
||||
|
||||
let attestation_service = self.clone();
|
||||
|
||||
let attestation_data_handle = self
|
||||
let mut attestation_data_from_head_event = None;
|
||||
|
||||
if let Some((beacon_node_index, expected_block_root)) = beacon_node_data {
|
||||
match attestation_service
|
||||
.beacon_nodes
|
||||
.run_on_candidate_index(beacon_node_index, |beacon_node| async move {
|
||||
let _timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[validator_metrics::ATTESTATIONS_HTTP_GET],
|
||||
);
|
||||
let data = beacon_node
|
||||
.get_validator_attestation_data(slot, 0)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to produce attestation data: {:?}", e))?
|
||||
.data;
|
||||
|
||||
if data.beacon_block_root != expected_block_root {
|
||||
return Err(format!(
|
||||
"Attestation block root mismatch: expected {:?}, got {:?}",
|
||||
expected_block_root, data.beacon_block_root
|
||||
));
|
||||
}
|
||||
Ok(data)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(data) => attestation_data_from_head_event = Some(data),
|
||||
Err(error) => {
|
||||
warn!(?error, "Failed to attest based on head event");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the beacon node that sent us the head failed to attest, wait until the attestation
|
||||
// deadline then try all BNs.
|
||||
let attestation_data = if let Some(attestation_data) = attestation_data_from_head_event {
|
||||
attestation_data
|
||||
} else {
|
||||
let duration_to_deadline = self
|
||||
.slot_clock
|
||||
.duration_to_slot(slot + 1)
|
||||
.and_then(|duration_to_next_slot| {
|
||||
duration_to_next_slot
|
||||
.checked_add(self.chain_spec.get_unaggregated_attestation_due())
|
||||
})
|
||||
.map(|next_slot_deadline| {
|
||||
next_slot_deadline.saturating_sub(self.chain_spec.get_slot_duration())
|
||||
})
|
||||
.unwrap_or(Duration::from_secs(0));
|
||||
sleep(duration_to_deadline).await;
|
||||
|
||||
attestation_service
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let _timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[validator_metrics::ATTESTATIONS_HTTP_GET],
|
||||
);
|
||||
let data = beacon_node
|
||||
.get_validator_attestation_data(slot, 0)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to produce attestation data: {:?}", e))?
|
||||
.data;
|
||||
Ok::<AttestationData, String>(data)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
// Sign and publish attestations.
|
||||
let publication_handle = self
|
||||
.inner
|
||||
.executor
|
||||
.spawn_handle(
|
||||
async move {
|
||||
let attestation_data = attestation_service
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let _timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[validator_metrics::ATTESTATIONS_HTTP_GET],
|
||||
);
|
||||
beacon_node
|
||||
.get_validator_attestation_data(slot, 0)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to produce attestation data: {:?}", e))
|
||||
.map(|result| result.data)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
attestation_service
|
||||
.sign_and_publish_attestations(
|
||||
slot,
|
||||
@@ -231,7 +355,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crit!(
|
||||
error = format!("{:?}", e),
|
||||
error = e,
|
||||
slot = slot.as_u64(),
|
||||
"Error during attestation routine"
|
||||
);
|
||||
@@ -239,15 +363,20 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
})?;
|
||||
Ok::<AttestationData, String>(attestation_data)
|
||||
},
|
||||
"unaggregated attestation production",
|
||||
"unaggregated attestation publication",
|
||||
)
|
||||
.ok_or("Failed to spawn attestation data task")?;
|
||||
|
||||
// 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 duration_to_next_slot = self
|
||||
.slot_clock
|
||||
.duration_to_slot(slot + 1)
|
||||
.ok_or("Unable to determine duration to next slot")?;
|
||||
let aggregate_production_instant = Instant::now()
|
||||
+ duration_to_next_slot
|
||||
.checked_sub(slot_duration / 3)
|
||||
.checked_add(self.chain_spec.get_aggregate_attestation_due())
|
||||
.and_then(|offset| offset.checked_sub(self.chain_spec.get_slot_duration()))
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
let aggregate_duties_by_committee_index: HashMap<CommitteeIndex, Vec<DutyAndProof>> = self
|
||||
@@ -267,7 +396,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
self.inner.executor.spawn(
|
||||
async move {
|
||||
// Log an error if the handle fails and return, skipping aggregates
|
||||
let attestation_data = match attestation_data_handle.await {
|
||||
let attestation_data = match publication_handle.await {
|
||||
Ok(Some(Ok(data))) => data,
|
||||
Ok(Some(Err(err))) => {
|
||||
error!(?err, "Attestation production failed");
|
||||
@@ -310,7 +439,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "handle_aggregates",
|
||||
name = "lh_handle_aggregates",
|
||||
skip_all,
|
||||
fields(%slot, %committee_index)
|
||||
)]
|
||||
@@ -365,7 +494,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
///
|
||||
/// The given `validator_duties` should already be filtered to only contain those that match
|
||||
/// `slot`. Critical errors will be logged if this is not the case.
|
||||
#[instrument(skip_all, fields(%slot, %attestation_data.beacon_block_root))]
|
||||
#[instrument(name = "lh_sign_and_publish_attestations", skip_all, fields(%slot, %attestation_data.beacon_block_root))]
|
||||
async fn sign_and_publish_attestations(
|
||||
&self,
|
||||
slot: Slot,
|
||||
@@ -383,160 +512,157 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
.ok_or("Unable to determine current slot from clock")?
|
||||
.epoch(S::E::slots_per_epoch());
|
||||
|
||||
// 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;
|
||||
// Make sure the target epoch is not higher than the current epoch to avoid potential attacks.
|
||||
if attestation_data.target.epoch > current_epoch {
|
||||
return Err(format!(
|
||||
"Attestation target epoch {} is higher than current epoch {}",
|
||||
attestation_data.target.epoch, current_epoch
|
||||
));
|
||||
}
|
||||
|
||||
// Ensure that the attestation matches the duties.
|
||||
if !duty.match_attestation_data::<S::E>(attestation_data, &self.chain_spec) {
|
||||
// Create attestations for each validator duty.
|
||||
let mut attestations_to_sign = Vec::with_capacity(validator_duties.len());
|
||||
|
||||
for duty_and_proof in validator_duties {
|
||||
let duty = &duty_and_proof.duty;
|
||||
|
||||
// Ensure that the attestation matches the duties.
|
||||
if !duty.match_attestation_data::<S::E>(&attestation_data, &self.chain_spec) {
|
||||
crit!(
|
||||
validator = ?duty.pubkey,
|
||||
duty_slot = %duty.slot,
|
||||
attestation_slot = %attestation_data.slot,
|
||||
duty_index = duty.committee_index,
|
||||
attestation_index = attestation_data.index,
|
||||
"Inconsistent validator duties during signing"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let attestation = match Attestation::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,
|
||||
attestation_data.index != 0,
|
||||
&self.chain_spec,
|
||||
) {
|
||||
Ok(attestation) => attestation,
|
||||
Err(err) => {
|
||||
crit!(
|
||||
validator = ?duty.pubkey,
|
||||
duty_slot = %duty.slot,
|
||||
attestation_slot = %attestation_data.slot,
|
||||
duty_index = duty.committee_index,
|
||||
attestation_index = attestation_data.index,
|
||||
"Inconsistent validator duties during signing"
|
||||
?duty,
|
||||
?err,
|
||||
"Invalid validator duties during signing"
|
||||
);
|
||||
return None;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut attestation = match Attestation::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.chain_spec,
|
||||
) {
|
||||
Ok(attestation) => attestation,
|
||||
Err(err) => {
|
||||
crit!(
|
||||
validator = ?duty.pubkey,
|
||||
?duty,
|
||||
?err,
|
||||
"Invalid validator duties during signing"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
attestations_to_sign.push(AttestationToSign {
|
||||
validator_index: duty.validator_index,
|
||||
pubkey: duty.pubkey,
|
||||
validator_committee_index: duty.validator_committee_index as usize,
|
||||
attestation,
|
||||
});
|
||||
}
|
||||
|
||||
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!(
|
||||
info = "a validator may have recently been removed from this VC",
|
||||
pubkey = ?pubkey,
|
||||
validator = ?duty.pubkey,
|
||||
slot = slot.as_u64(),
|
||||
"Missing pubkey for attestation"
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
error = ?e,
|
||||
validator = ?duty.pubkey,
|
||||
slot = slot.as_u64(),
|
||||
"Failed to sign attestation"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
.instrument(Span::current())
|
||||
});
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let (ref attestations, ref validator_indices): (Vec<_>, Vec<_>) = join_all(signing_futures)
|
||||
.instrument(info_span!(
|
||||
"sign_attestations",
|
||||
count = validator_duties.len()
|
||||
))
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.unzip();
|
||||
|
||||
if attestations.is_empty() {
|
||||
warn!("No attestations were published");
|
||||
if attestations_to_sign.is_empty() {
|
||||
warn!("No valid attestations to sign");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let attestation_stream = self.validator_store.sign_attestations(attestations_to_sign);
|
||||
tokio::pin!(attestation_stream);
|
||||
|
||||
let fork_name = self
|
||||
.chain_spec
|
||||
.fork_name_at_slot::<S::E>(attestation_data.slot);
|
||||
|
||||
// Post the attestations to the BN.
|
||||
match self
|
||||
.beacon_nodes
|
||||
.request(ApiTopic::Attestations, |beacon_node| async move {
|
||||
let _timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[validator_metrics::ATTESTATIONS_HTTP_POST],
|
||||
);
|
||||
// Publish each batch as it arrives from the stream.
|
||||
let mut received_non_empty_batch = false;
|
||||
while let Some(result) = attestation_stream.next().await {
|
||||
match result {
|
||||
Ok(batch) if !batch.is_empty() => {
|
||||
received_non_empty_batch = true;
|
||||
|
||||
let single_attestations = attestations
|
||||
.iter()
|
||||
.zip(validator_indices)
|
||||
.filter_map(|(a, i)| {
|
||||
match a.to_single_attestation_with_attester_index(*i) {
|
||||
Ok(a) => Some(a),
|
||||
Err(e) => {
|
||||
// This shouldn't happen unless BN and VC are out of sync with
|
||||
// respect to the Electra fork.
|
||||
error!(
|
||||
error = ?e,
|
||||
committee_index = attestation_data.index,
|
||||
slot = slot.as_u64(),
|
||||
"type" = "unaggregated",
|
||||
"Unable to convert to SingleAttestation"
|
||||
);
|
||||
None
|
||||
let single_attestations = batch
|
||||
.iter()
|
||||
.filter_map(|(attester_index, attestation)| {
|
||||
match attestation
|
||||
.to_single_attestation_with_attester_index(*attester_index)
|
||||
{
|
||||
Ok(single_attestation) => Some(single_attestation),
|
||||
Err(e) => {
|
||||
// This shouldn't happen unless BN and VC are out of sync with
|
||||
// respect to the Electra fork.
|
||||
error!(
|
||||
error = ?e,
|
||||
committee_index = attestation_data.index,
|
||||
slot = slot.as_u64(),
|
||||
"type" = "unaggregated",
|
||||
"Unable to convert to SingleAttestation"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let single_attestations = &single_attestations;
|
||||
let validator_indices = single_attestations
|
||||
.iter()
|
||||
.map(|att| att.attester_index)
|
||||
.collect::<Vec<_>>();
|
||||
let published_count = single_attestations.len();
|
||||
|
||||
beacon_node
|
||||
.post_beacon_pool_attestations_v2::<S::E>(single_attestations, fork_name)
|
||||
.await
|
||||
})
|
||||
.instrument(info_span!(
|
||||
"publish_attestations",
|
||||
count = attestations.len()
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(()) => info!(
|
||||
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",
|
||||
"Successfully published attestations"
|
||||
),
|
||||
Err(e) => error!(
|
||||
error = %e,
|
||||
committee_index = attestation_data.index,
|
||||
slot = slot.as_u64(),
|
||||
"type" = "unaggregated",
|
||||
"Unable to publish attestations"
|
||||
),
|
||||
// Post the attestations to the BN.
|
||||
match self
|
||||
.beacon_nodes
|
||||
.request(ApiTopic::Attestations, |beacon_node| async move {
|
||||
let _timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[validator_metrics::ATTESTATIONS_HTTP_POST],
|
||||
);
|
||||
|
||||
beacon_node
|
||||
.post_beacon_pool_attestations_v2::<S::E>(
|
||||
single_attestations.clone(),
|
||||
fork_name,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.instrument(info_span!("publish_attestations", count = published_count))
|
||||
.await
|
||||
{
|
||||
Ok(()) => info!(
|
||||
count = published_count,
|
||||
validator_indices = ?validator_indices,
|
||||
head_block = ?attestation_data.beacon_block_root,
|
||||
committee_index = attestation_data.index,
|
||||
slot = attestation_data.slot.as_u64(),
|
||||
"type" = "unaggregated",
|
||||
"Successfully published attestations"
|
||||
),
|
||||
Err(e) => error!(
|
||||
error = %e,
|
||||
committee_index = attestation_data.index,
|
||||
slot = slot.as_u64(),
|
||||
"type" = "unaggregated",
|
||||
"Unable to publish attestations"
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(error = ?e, "Failed to sign attestations");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !received_non_empty_batch {
|
||||
warn!("No attestations were published");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -612,113 +738,103 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
|
||||
.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::<S::E>(attestation_data, &self.chain_spec) {
|
||||
crit!("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!(?pubkey, "Missing pubkey for aggregate");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
error = ?e,
|
||||
pubkey = ?duty.pubkey,
|
||||
"Failed to sign aggregate"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let aggregator_count = validator_duties
|
||||
// Build the batch of aggregates to sign.
|
||||
let aggregates_to_sign: Vec<_> = validator_duties
|
||||
.iter()
|
||||
.filter(|d| d.selection_proof.is_some())
|
||||
.count();
|
||||
let signed_aggregate_and_proofs = join_all(signing_futures)
|
||||
.instrument(info_span!("sign_aggregates", count = aggregator_count))
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
.filter_map(|duty_and_proof| {
|
||||
let duty = &duty_and_proof.duty;
|
||||
let selection_proof = duty_and_proof.selection_proof.as_ref()?;
|
||||
|
||||
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 = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[validator_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
|
||||
}
|
||||
if !duty.match_attestation_data::<S::E>(attestation_data, &self.chain_spec) {
|
||||
crit!("Inconsistent validator duties during signing");
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(AggregateToSign {
|
||||
pubkey: duty.pubkey,
|
||||
aggregator_index: duty.validator_index,
|
||||
aggregate: aggregated_attestation.clone(),
|
||||
selection_proof: selection_proof.clone(),
|
||||
})
|
||||
.instrument(info_span!(
|
||||
"publish_aggregates",
|
||||
count = signed_aggregate_and_proofs.len()
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
for signed_aggregate_and_proof in signed_aggregate_and_proofs {
|
||||
let attestation = signed_aggregate_and_proof.message().aggregate();
|
||||
info!(
|
||||
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",
|
||||
"Successfully published attestation"
|
||||
);
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sign aggregates. Returns a stream of batches.
|
||||
let aggregate_stream = self
|
||||
.validator_store
|
||||
.sign_aggregate_and_proofs(aggregates_to_sign);
|
||||
tokio::pin!(aggregate_stream);
|
||||
|
||||
// Publish each batch as it arrives from the stream.
|
||||
while let Some(result) = aggregate_stream.next().await {
|
||||
match result {
|
||||
Ok(batch) if !batch.is_empty() => {
|
||||
let signed_aggregate_and_proofs = batch.as_slice();
|
||||
match self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let _timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::ATTESTATION_SERVICE_TIMES,
|
||||
&[validator_metrics::AGGREGATES_HTTP_POST],
|
||||
);
|
||||
if fork_name.electra_enabled() {
|
||||
beacon_node
|
||||
.post_validator_aggregate_and_proof_v2(
|
||||
signed_aggregate_and_proofs,
|
||||
fork_name,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
beacon_node
|
||||
.post_validator_aggregate_and_proof_v1(
|
||||
signed_aggregate_and_proofs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.instrument(info_span!(
|
||||
"publish_aggregates",
|
||||
count = signed_aggregate_and_proofs.len()
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
for signed_aggregate_and_proof in signed_aggregate_and_proofs {
|
||||
let attestation = signed_aggregate_and_proof.message().aggregate();
|
||||
info!(
|
||||
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",
|
||||
"Successfully published attestation"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
for signed_aggregate_and_proof in signed_aggregate_and_proofs {
|
||||
let attestation = &signed_aggregate_and_proof.message().aggregate();
|
||||
crit!(
|
||||
error = %e,
|
||||
aggregator = signed_aggregate_and_proof
|
||||
.message()
|
||||
.aggregator_index(),
|
||||
committee_index = attestation.committee_index(),
|
||||
slot = attestation.data().slot.as_u64(),
|
||||
"type" = "aggregated",
|
||||
"Failed to publish attestation"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
for signed_aggregate_and_proof in signed_aggregate_and_proofs {
|
||||
let attestation = &signed_aggregate_and_proof.message().aggregate();
|
||||
crit!(
|
||||
error = %e,
|
||||
aggregator = signed_aggregate_and_proof.message().aggregator_index(),
|
||||
committee_index = attestation.committee_index(),
|
||||
slot = attestation.data().slot.as_u64(),
|
||||
"type" = "aggregated",
|
||||
"Failed to publish attestation"
|
||||
);
|
||||
}
|
||||
crit!(error = ?e, "Failed to sign aggregates");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, Error as FallbackError, Errors};
|
||||
use bls::PublicKeyBytes;
|
||||
use eth2::BeaconNodeHttpClient;
|
||||
use eth2::types::GraffitiPolicy;
|
||||
use eth2::{BeaconNodeHttpClient, StatusCode};
|
||||
use graffiti_file::{GraffitiFile, determine_graffiti};
|
||||
use logging::crit;
|
||||
use reqwest::StatusCode;
|
||||
use slot_clock::SlotClock;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
@@ -13,6 +14,7 @@ use std::time::Duration;
|
||||
use task_executor::TaskExecutor;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn};
|
||||
use types::consts::gloas::BUILDER_INDEX_SELF_BUILD;
|
||||
use types::{BlockType, ChainSpec, EthSpec, Graffiti, Slot};
|
||||
use validator_store::{Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore};
|
||||
|
||||
@@ -333,7 +335,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> {
|
||||
#[instrument(skip_all, fields(%slot, ?validator_pubkey))]
|
||||
async fn sign_and_publish_block(
|
||||
&self,
|
||||
proposer_fallback: ProposerFallback<T>,
|
||||
proposer_fallback: &ProposerFallback<T>,
|
||||
slot: Slot,
|
||||
graffiti: Option<Graffiti>,
|
||||
validator_pubkey: &PublicKeyBytes,
|
||||
@@ -402,7 +404,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> {
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "block_proposal_duty_cycle",
|
||||
name = "lh_block_proposal_duty_cycle",
|
||||
skip_all,
|
||||
fields(%slot, ?validator_pubkey)
|
||||
)]
|
||||
@@ -459,73 +461,145 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> {
|
||||
|
||||
info!(slot = slot.as_u64(), "Requesting unsigned block");
|
||||
|
||||
// Request an SSZ block from all beacon nodes in order, returning on the first successful response.
|
||||
// If all nodes fail, run a second pass falling back to JSON.
|
||||
//
|
||||
// Proposer nodes will always be tried last during each pass since it's likely that they don't have a
|
||||
// great view of attestations on the network.
|
||||
let ssz_block_response = proposer_fallback
|
||||
.request_proposers_last(|beacon_node| async move {
|
||||
let _get_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::BLOCK_SERVICE_TIMES,
|
||||
&[validator_metrics::BEACON_BLOCK_HTTP_GET],
|
||||
);
|
||||
beacon_node
|
||||
.get_validator_blocks_v3_ssz::<S::E>(
|
||||
slot,
|
||||
randao_reveal_ref,
|
||||
graffiti.as_ref(),
|
||||
builder_boost_factor,
|
||||
self_ref.graffiti_policy,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
// Check if Gloas fork is active at this slot
|
||||
let fork_name = self_ref.chain_spec.fork_name_at_slot::<S::E>(slot);
|
||||
|
||||
let block_response = match ssz_block_response {
|
||||
Ok((ssz_block_response, _metadata)) => ssz_block_response,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
slot = slot.as_u64(),
|
||||
error = %e,
|
||||
"SSZ block production failed, falling back to JSON"
|
||||
);
|
||||
let (block_proposer, unsigned_block) = if fork_name.gloas_enabled() {
|
||||
// Use V4 block production for Gloas
|
||||
// Request an SSZ block from all beacon nodes in order, returning on the first successful response.
|
||||
// If all nodes fail, run a second pass falling back to JSON.
|
||||
let ssz_block_response = proposer_fallback
|
||||
.request_proposers_last(|beacon_node| async move {
|
||||
let _get_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::BLOCK_SERVICE_TIMES,
|
||||
&[validator_metrics::BEACON_BLOCK_HTTP_GET],
|
||||
);
|
||||
beacon_node
|
||||
.get_validator_blocks_v4_ssz::<S::E>(
|
||||
slot,
|
||||
randao_reveal_ref,
|
||||
graffiti.as_ref(),
|
||||
builder_boost_factor,
|
||||
self_ref.graffiti_policy,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
proposer_fallback
|
||||
.request_proposers_last(|beacon_node| async move {
|
||||
let _get_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::BLOCK_SERVICE_TIMES,
|
||||
&[validator_metrics::BEACON_BLOCK_HTTP_GET],
|
||||
);
|
||||
let (json_block_response, _metadata) = beacon_node
|
||||
.get_validator_blocks_v3::<S::E>(
|
||||
slot,
|
||||
randao_reveal_ref,
|
||||
graffiti.as_ref(),
|
||||
builder_boost_factor,
|
||||
self_ref.graffiti_policy,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
BlockError::Recoverable(format!(
|
||||
"Error from beacon node when producing block: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let block_response = match ssz_block_response {
|
||||
Ok((ssz_block_response, _metadata)) => ssz_block_response,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
slot = slot.as_u64(),
|
||||
error = %e,
|
||||
"SSZ V4 block production failed, falling back to JSON"
|
||||
);
|
||||
|
||||
Ok(json_block_response.data)
|
||||
})
|
||||
.await
|
||||
.map_err(BlockError::from)?
|
||||
}
|
||||
};
|
||||
proposer_fallback
|
||||
.request_proposers_last(|beacon_node| async move {
|
||||
let _get_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::BLOCK_SERVICE_TIMES,
|
||||
&[validator_metrics::BEACON_BLOCK_HTTP_GET],
|
||||
);
|
||||
let (json_block_response, _metadata) = beacon_node
|
||||
.get_validator_blocks_v4::<S::E>(
|
||||
slot,
|
||||
randao_reveal_ref,
|
||||
graffiti.as_ref(),
|
||||
builder_boost_factor,
|
||||
self_ref.graffiti_policy,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
BlockError::Recoverable(format!(
|
||||
"Error from beacon node when producing block: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let (block_proposer, unsigned_block) = match block_response {
|
||||
eth2::types::ProduceBlockV3Response::Full(block) => {
|
||||
(block.block().proposer_index(), UnsignedBlock::Full(block))
|
||||
}
|
||||
eth2::types::ProduceBlockV3Response::Blinded(block) => {
|
||||
(block.proposer_index(), UnsignedBlock::Blinded(block))
|
||||
Ok(json_block_response.data)
|
||||
})
|
||||
.await
|
||||
.map_err(BlockError::from)?
|
||||
}
|
||||
};
|
||||
|
||||
// Gloas blocks don't have blobs (they're in the execution layer)
|
||||
let block_contents = eth2::types::FullBlockContents::Block(block_response);
|
||||
(
|
||||
block_contents.block().proposer_index(),
|
||||
UnsignedBlock::Full(block_contents),
|
||||
)
|
||||
} else {
|
||||
// Use V3 block production for pre-Gloas forks
|
||||
// Request an SSZ block from all beacon nodes in order, returning on the first successful response.
|
||||
// If all nodes fail, run a second pass falling back to JSON.
|
||||
//
|
||||
// Proposer nodes will always be tried last during each pass since it's likely that they don't have a
|
||||
// great view of attestations on the network.
|
||||
let ssz_block_response = proposer_fallback
|
||||
.request_proposers_last(|beacon_node| async move {
|
||||
let _get_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::BLOCK_SERVICE_TIMES,
|
||||
&[validator_metrics::BEACON_BLOCK_HTTP_GET],
|
||||
);
|
||||
beacon_node
|
||||
.get_validator_blocks_v3_ssz::<S::E>(
|
||||
slot,
|
||||
randao_reveal_ref,
|
||||
graffiti.as_ref(),
|
||||
builder_boost_factor,
|
||||
self_ref.graffiti_policy,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
let block_response = match ssz_block_response {
|
||||
Ok((ssz_block_response, _metadata)) => ssz_block_response,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
slot = slot.as_u64(),
|
||||
error = %e,
|
||||
"SSZ block production failed, falling back to JSON"
|
||||
);
|
||||
|
||||
proposer_fallback
|
||||
.request_proposers_last(|beacon_node| async move {
|
||||
let _get_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::BLOCK_SERVICE_TIMES,
|
||||
&[validator_metrics::BEACON_BLOCK_HTTP_GET],
|
||||
);
|
||||
let (json_block_response, _metadata) = beacon_node
|
||||
.get_validator_blocks_v3::<S::E>(
|
||||
slot,
|
||||
randao_reveal_ref,
|
||||
graffiti.as_ref(),
|
||||
builder_boost_factor,
|
||||
self_ref.graffiti_policy,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
BlockError::Recoverable(format!(
|
||||
"Error from beacon node when producing block: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(json_block_response.data)
|
||||
})
|
||||
.await
|
||||
.map_err(BlockError::from)?
|
||||
}
|
||||
};
|
||||
|
||||
match block_response {
|
||||
eth2::types::ProduceBlockV3Response::Full(block) => {
|
||||
(block.block().proposer_index(), UnsignedBlock::Full(block))
|
||||
}
|
||||
eth2::types::ProduceBlockV3Response::Blinded(block) => {
|
||||
(block.proposer_index(), UnsignedBlock::Blinded(block))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -538,7 +612,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> {
|
||||
|
||||
self_ref
|
||||
.sign_and_publish_block(
|
||||
proposer_fallback,
|
||||
&proposer_fallback,
|
||||
slot,
|
||||
graffiti,
|
||||
&validator_pubkey,
|
||||
@@ -546,6 +620,108 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO(gloas) we only need to fetch, sign and publish the envelope in the local building case.
|
||||
// Right now we always default to local building. Once we implement trustless/trusted builder logic
|
||||
// we should check the bid for index == BUILDER_INDEX_SELF_BUILD
|
||||
if fork_name.gloas_enabled() {
|
||||
self_ref
|
||||
.fetch_sign_and_publish_payload_envelope(
|
||||
&proposer_fallback,
|
||||
slot,
|
||||
&validator_pubkey,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch, sign, and publish the execution payload envelope for Gloas.
|
||||
/// This should be called after the block has been published.
|
||||
///
|
||||
/// TODO(gloas): For multi-BN setups, we need to track which beacon node produced the block
|
||||
/// and fetch the envelope from that same node. The envelope is cached per-BN,
|
||||
/// so fetching from a different BN than the one that built the block will fail.
|
||||
/// See: https://github.com/sigp/lighthouse/pull/8313
|
||||
#[instrument(skip_all)]
|
||||
async fn fetch_sign_and_publish_payload_envelope(
|
||||
&self,
|
||||
_proposer_fallback: &ProposerFallback<T>,
|
||||
slot: Slot,
|
||||
validator_pubkey: &PublicKeyBytes,
|
||||
) -> Result<(), BlockError> {
|
||||
info!(slot = slot.as_u64(), "Fetching execution payload envelope");
|
||||
|
||||
// Fetch the envelope from the beacon node. Use builder_index=BUILDER_INDEX_SELF_BUILD for local building.
|
||||
// TODO(gloas): Use proposer_fallback once multi-BN is supported.
|
||||
let envelope = self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
beacon_node
|
||||
.get_validator_execution_payload_envelope_ssz::<S::E>(
|
||||
slot,
|
||||
BUILDER_INDEX_SELF_BUILD,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
BlockError::Recoverable(format!(
|
||||
"Error fetching execution payload envelope: {:?}",
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
slot = slot.as_u64(),
|
||||
beacon_block_root = %envelope.beacon_block_root,
|
||||
"Received execution payload envelope, signing"
|
||||
);
|
||||
|
||||
// Sign the envelope
|
||||
let signed_envelope = self
|
||||
.validator_store
|
||||
.sign_execution_payload_envelope(*validator_pubkey, envelope)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
BlockError::Recoverable(format!(
|
||||
"Error signing execution payload envelope: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
info!(
|
||||
slot = slot.as_u64(),
|
||||
"Signed execution payload envelope, publishing"
|
||||
);
|
||||
|
||||
let fork_name = self.chain_spec.fork_name_at_slot::<S::E>(slot);
|
||||
|
||||
// Publish the signed envelope
|
||||
// TODO(gloas): Use proposer_fallback once multi-BN is supported.
|
||||
self.beacon_nodes
|
||||
.first_success(|beacon_node| {
|
||||
let signed_envelope = signed_envelope.clone();
|
||||
async move {
|
||||
beacon_node
|
||||
.post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
BlockError::Recoverable(format!(
|
||||
"Error publishing execution payload envelope: {:?}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
slot = slot.as_u64(),
|
||||
beacon_block_root = %signed_envelope.message.beacon_block_root,
|
||||
"Successfully published signed execution payload envelope"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback};
|
||||
use bls::PublicKeyBytes;
|
||||
use eth2::types::{
|
||||
AttesterData, BeaconCommitteeSelection, BeaconCommitteeSubscription, DutiesResponse,
|
||||
InclusionListDuty, ProposerData, StateId, ValidatorId,
|
||||
InclusionListDuty, ProposerData, PtcDuty, StateId, ValidatorId,
|
||||
};
|
||||
use futures::{
|
||||
StreamExt,
|
||||
stream::{self, FuturesUnordered},
|
||||
stream::{self, FuturesUnordered, TryStreamExt},
|
||||
};
|
||||
use parking_lot::{RwLock, RwLockWriteGuard};
|
||||
use safe_arith::{ArithError, SafeArith};
|
||||
@@ -46,6 +46,7 @@ const VALIDATOR_METRICS_MIN_COUNT: usize = 64;
|
||||
/// The initial request is used to determine if further requests are required, so that it
|
||||
/// reduces the amount of data that needs to be transferred.
|
||||
const INITIAL_DUTIES_QUERY_SIZE: usize = 1;
|
||||
const INITIAL_PTC_DUTIES_QUERY_SIZE: usize = 1;
|
||||
|
||||
/// Offsets from the attestation duty slot at which a subscription should be sent.
|
||||
const ATTESTATION_SUBSCRIPTION_OFFSETS: [u64; 8] = [3, 4, 5, 6, 7, 8, 16, 32];
|
||||
@@ -83,6 +84,7 @@ const _: () = assert!(ATTESTATION_SUBSCRIPTION_OFFSETS[0] > MIN_ATTESTATION_SUBS
|
||||
pub enum Error<T> {
|
||||
UnableToReadSlotClock,
|
||||
FailedToDownloadAttesters(#[allow(dead_code)] String),
|
||||
FailedToDownloadPtc(#[allow(dead_code)] String),
|
||||
FailedToProduceSelectionProof(#[allow(dead_code)] ValidatorStoreError<T>),
|
||||
InvalidModulo(#[allow(dead_code)] ArithError),
|
||||
Arith(#[allow(dead_code)] ArithError),
|
||||
@@ -142,71 +144,15 @@ impl Default for SelectionProofConfig {
|
||||
/// Create a selection proof for `duty`.
|
||||
///
|
||||
/// Return `Ok(None)` if the attesting validator is not an aggregator.
|
||||
async fn make_selection_proof<S: ValidatorStore + 'static, T: SlotClock>(
|
||||
async fn make_selection_proof<S: ValidatorStore>(
|
||||
duty: &AttesterData,
|
||||
validator_store: &S,
|
||||
spec: &ChainSpec,
|
||||
beacon_nodes: &Arc<BeaconNodeFallback<T>>,
|
||||
config: &SelectionProofConfig,
|
||||
) -> Result<Option<SelectionProof>, Error<S::Error>> {
|
||||
let selection_proof = if config.selections_endpoint {
|
||||
let beacon_committee_selection = BeaconCommitteeSelection {
|
||||
validator_index: duty.validator_index,
|
||||
slot: duty.slot,
|
||||
// This is partial selection proof
|
||||
selection_proof: validator_store
|
||||
.produce_selection_proof(duty.pubkey, duty.slot)
|
||||
.await
|
||||
.map_err(Error::FailedToProduceSelectionProof)?
|
||||
.into(),
|
||||
};
|
||||
// Call the endpoint /eth/v1/validator/beacon_committee_selections
|
||||
// by sending the BeaconCommitteeSelection that contains partial selection proof
|
||||
// The middleware should return BeaconCommitteeSelection that contains full selection proof
|
||||
let middleware_response = beacon_nodes
|
||||
.first_success(|beacon_node| {
|
||||
let selection_data = beacon_committee_selection.clone();
|
||||
debug!(
|
||||
"validator_index" = duty.validator_index,
|
||||
"slot" = %duty.slot,
|
||||
"partial selection proof" = ?beacon_committee_selection.selection_proof,
|
||||
"Sending selection to middleware"
|
||||
);
|
||||
async move {
|
||||
beacon_node
|
||||
.post_validator_beacon_committee_selections(&[selection_data])
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let response_data = middleware_response
|
||||
.map_err(|e| {
|
||||
Error::FailedToProduceSelectionProof(ValidatorStoreError::Middleware(e.to_string()))
|
||||
})?
|
||||
.data
|
||||
.pop()
|
||||
.ok_or_else(|| {
|
||||
Error::FailedToProduceSelectionProof(ValidatorStoreError::Middleware(format!(
|
||||
"attestation selection proof - empty response for validator {}",
|
||||
duty.validator_index
|
||||
)))
|
||||
})?;
|
||||
|
||||
debug!(
|
||||
"validator_index" = response_data.validator_index,
|
||||
"slot" = %response_data.slot,
|
||||
// The selection proof from middleware response will be a full selection proof
|
||||
"full selection proof" = ?response_data.selection_proof,
|
||||
"Received selection from middleware"
|
||||
);
|
||||
SelectionProof::from(response_data.selection_proof)
|
||||
} else {
|
||||
validator_store
|
||||
.produce_selection_proof(duty.pubkey, duty.slot)
|
||||
.await
|
||||
.map_err(Error::FailedToProduceSelectionProof)?
|
||||
};
|
||||
let selection_proof = validator_store
|
||||
.produce_selection_proof(duty.pubkey, duty.slot)
|
||||
.await
|
||||
.map_err(Error::FailedToProduceSelectionProof)?;
|
||||
|
||||
selection_proof
|
||||
.is_aggregator(duty.committee_length as usize, spec)
|
||||
@@ -222,6 +168,69 @@ async fn make_selection_proof<S: ValidatorStore + 'static, T: SlotClock>(
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a Vec<BeaconCommitteeSelection> for every epoch
|
||||
/// so that when calling the selections_endpoint later, it calls once per epoch with duties of all slots in that epoch
|
||||
async fn make_beacon_committee_selection<S: ValidatorStore, T: SlotClock>(
|
||||
duties_service: &Arc<DutiesService<S, T>>,
|
||||
duties: &[AttesterData],
|
||||
) -> Result<Vec<BeaconCommitteeSelection>, Error<S::Error>> {
|
||||
// collect the BeaconCommitteeSelection in duties
|
||||
let beacon_committee_selections = duties
|
||||
.iter()
|
||||
.map(|duty| {
|
||||
let validator_store = &duties_service.validator_store;
|
||||
async move {
|
||||
let partial_selection_proof = validator_store
|
||||
.produce_selection_proof(duty.pubkey, duty.slot)
|
||||
.await
|
||||
.map_err(Error::FailedToProduceSelectionProof)?;
|
||||
Ok::<BeaconCommitteeSelection, Error<S::Error>>(BeaconCommitteeSelection {
|
||||
validator_index: duty.validator_index,
|
||||
slot: duty.slot,
|
||||
selection_proof: partial_selection_proof.into(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
let epoch = duties
|
||||
.first()
|
||||
.map(|attester_data| attester_data.slot.epoch(S::E::slots_per_epoch()))
|
||||
.unwrap_or_default();
|
||||
|
||||
debug!(
|
||||
%epoch,
|
||||
count = beacon_committee_selections.len(),
|
||||
"Sending selections to middleware"
|
||||
);
|
||||
|
||||
let selections = duties_service
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| {
|
||||
let selections = beacon_committee_selections.clone();
|
||||
async move {
|
||||
beacon_node
|
||||
.post_validator_beacon_committee_selections(&selections)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::FailedToProduceSelectionProof(ValidatorStoreError::Middleware(e.to_string()))
|
||||
})?
|
||||
.data;
|
||||
|
||||
debug!(
|
||||
%epoch,
|
||||
count = beacon_committee_selections.len(),
|
||||
"Received selections from middleware"
|
||||
);
|
||||
|
||||
Ok(selections)
|
||||
}
|
||||
|
||||
impl DutyAndProof {
|
||||
/// Create a new `DutyAndProof` with the selection proof waiting to be filled in.
|
||||
pub fn new_without_selection_proof(duty: AttesterData, current_slot: Slot) -> Self {
|
||||
@@ -279,6 +288,7 @@ type AttesterMap = HashMap<PublicKeyBytes, HashMap<Epoch, (DependentRoot, DutyAn
|
||||
type ProposerMap = HashMap<Epoch, (DependentRoot, Vec<ProposerData>)>;
|
||||
type InclusionListDutiesMap =
|
||||
HashMap<PublicKeyBytes, HashMap<Epoch, (DependentRoot, InclusionListDuty)>>;
|
||||
type PtcMap = HashMap<Epoch, (DependentRoot, Vec<PtcDuty>)>;
|
||||
|
||||
pub struct DutiesServiceBuilder<S, T> {
|
||||
/// Provides the canonical list of locally-managed validators.
|
||||
@@ -381,6 +391,7 @@ impl<S, T> DutiesServiceBuilder<S, T> {
|
||||
proposers: Default::default(),
|
||||
sync_duties: SyncDutiesMap::new(self.sync_selection_proof_config),
|
||||
inclusion_list_duties: Default::default(),
|
||||
ptc_duties: Default::default(),
|
||||
validator_store: self
|
||||
.validator_store
|
||||
.ok_or("Cannot build DutiesService without validator_store")?,
|
||||
@@ -413,6 +424,8 @@ pub struct DutiesService<S, T> {
|
||||
/// Maps a validator public key to their inclusion list committee duties for each epoch.
|
||||
pub inclusion_list_duties: RwLock<InclusionListDutiesMap>,
|
||||
pub sync_duties: SyncDutiesMap,
|
||||
/// Maps an epoch to PTC duties for locally-managed validators.
|
||||
pub ptc_duties: RwLock<PtcMap>,
|
||||
/// Provides the canonical list of locally-managed validators.
|
||||
pub validator_store: Arc<S>,
|
||||
/// Maps unknown validator pubkeys to the next slot time when a poll should be conducted again.
|
||||
@@ -464,13 +477,22 @@ impl<S: ValidatorStore, T: SlotClock + 'static> DutiesService<S, T> {
|
||||
.voting_pubkeys(DoppelgangerStatus::only_safe);
|
||||
self.attesters
|
||||
.read()
|
||||
.iter()
|
||||
.filter_map(|(_, map)| map.get(&epoch))
|
||||
.values()
|
||||
.filter_map(|map| map.get(&epoch))
|
||||
.map(|(_, duty_and_proof)| duty_and_proof)
|
||||
.filter(|duty_and_proof| signing_pubkeys.contains(&duty_and_proof.duty.pubkey))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Returns the total number of validators that have PTC duties in the given epoch.
|
||||
pub fn ptc_count(&self, epoch: Epoch) -> usize {
|
||||
self.ptc_duties
|
||||
.read()
|
||||
.get(&epoch)
|
||||
.map(|(_, duties)| duties.len())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the total number of validators that are in a doppelganger detection period.
|
||||
pub fn doppelganger_detecting_count(&self) -> usize {
|
||||
self.validator_store
|
||||
@@ -517,8 +539,8 @@ impl<S: ValidatorStore, T: SlotClock + 'static> DutiesService<S, T> {
|
||||
|
||||
self.attesters
|
||||
.read()
|
||||
.iter()
|
||||
.filter_map(|(_, map)| map.get(&epoch))
|
||||
.values()
|
||||
.filter_map(|map| map.get(&epoch))
|
||||
.map(|(_, duty_and_proof)| duty_and_proof)
|
||||
.filter(|duty_and_proof| {
|
||||
duty_and_proof.duty.slot == slot
|
||||
@@ -556,6 +578,25 @@ impl<S: ValidatorStore, T: SlotClock + 'static> DutiesService<S, T> {
|
||||
self.enable_high_validator_count_metrics
|
||||
|| self.total_validator_count() <= VALIDATOR_METRICS_MIN_COUNT
|
||||
}
|
||||
|
||||
/// Get PTC duties for a specific slot.
|
||||
///
|
||||
/// Returns duties for local validators who have PTC assignments at the given slot.
|
||||
pub fn get_ptc_duties_for_slot(&self, slot: Slot) -> Vec<PtcDuty> {
|
||||
let epoch = slot.epoch(S::E::slots_per_epoch());
|
||||
|
||||
self.ptc_duties
|
||||
.read()
|
||||
.get(&epoch)
|
||||
.map(|(_, ptc_duties)| {
|
||||
ptc_duties
|
||||
.iter()
|
||||
.filter(|ptc_duty| ptc_duty.slot == slot)
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the service that periodically polls the beacon node for validator duties. This will start
|
||||
@@ -711,6 +752,61 @@ pub fn start_update_service<S: ValidatorStore + 'static, T: SlotClock + 'static>
|
||||
},
|
||||
"duties_service_inclusion_list_committee",
|
||||
);
|
||||
|
||||
// Spawn the task which keeps track of local PTC duties.
|
||||
// Only start PTC duties service if Gloas fork is scheduled.
|
||||
if core_duties_service.spec.is_gloas_scheduled() {
|
||||
let duties_service = core_duties_service.clone();
|
||||
core_duties_service.executor.spawn(
|
||||
async move {
|
||||
loop {
|
||||
// Check if we've reached the Gloas fork epoch before polling
|
||||
let Some(current_slot) = duties_service.slot_clock.now() else {
|
||||
// Unable to read slot clock, sleep and try again
|
||||
sleep(duties_service.slot_clock.slot_duration()).await;
|
||||
continue;
|
||||
};
|
||||
|
||||
let current_epoch = current_slot.epoch(S::E::slots_per_epoch());
|
||||
let Some(gloas_fork_epoch) = duties_service.spec.gloas_fork_epoch else {
|
||||
// Gloas fork epoch not configured, should not reach here
|
||||
break;
|
||||
};
|
||||
|
||||
if current_epoch + 1 < gloas_fork_epoch {
|
||||
// Wait until the next slot and check again
|
||||
if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() {
|
||||
sleep(duration).await;
|
||||
} else {
|
||||
sleep(duties_service.slot_clock.slot_duration()).await;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = poll_beacon_ptc_attesters(&duties_service).await {
|
||||
error!(
|
||||
error = ?e,
|
||||
"Failed to poll PTC duties"
|
||||
);
|
||||
}
|
||||
|
||||
// Wait until the next slot before polling again.
|
||||
// This doesn't mean that the beacon node will get polled every slot
|
||||
// as the PTC duties service will return early if it deems it already has
|
||||
// enough information.
|
||||
if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() {
|
||||
sleep(duration).await;
|
||||
} else {
|
||||
// Just sleep for one slot if we are unable to read the system clock, this gives
|
||||
// us an opportunity for the clock to eventually come good.
|
||||
sleep(duties_service.slot_clock.slot_duration()).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
"duties_service_ptc",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate through all the voting pubkeys in the `ValidatorStore` and attempt to learn any unknown
|
||||
@@ -943,8 +1039,8 @@ async fn poll_beacon_attesters<S: ValidatorStore + 'static, T: SlotClock + 'stat
|
||||
duties_service
|
||||
.attesters
|
||||
.read()
|
||||
.iter()
|
||||
.filter_map(|(_, map)| map.get(epoch))
|
||||
.values()
|
||||
.filter_map(|map| map.get(epoch))
|
||||
.filter(|(_, duty_and_proof)| {
|
||||
duty_and_proof
|
||||
.subscription_slots
|
||||
@@ -1331,6 +1427,26 @@ fn process_duty_and_proof<S: ValidatorStore>(
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_validator_duties_ptc<S: ValidatorStore, T: SlotClock + 'static>(
|
||||
duties_service: &Arc<DutiesService<S, T>>,
|
||||
epoch: Epoch,
|
||||
validator_indices: &[u64],
|
||||
) -> Result<DutiesResponse<Vec<PtcDuty>>, Error<S::Error>> {
|
||||
duties_service
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
let _timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::DUTIES_SERVICE_TIMES,
|
||||
&[validator_metrics::PTC_DUTIES_HTTP_POST],
|
||||
);
|
||||
beacon_node
|
||||
.post_validator_duties_ptc(epoch, validator_indices)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::FailedToDownloadPtc(e.to_string()))
|
||||
}
|
||||
|
||||
/// Compute the attestation selection proofs for the `duties` and add them to the `attesters` map.
|
||||
///
|
||||
/// Duties are computed in batches each slot. If a re-org is detected then the process will
|
||||
@@ -1343,14 +1459,21 @@ async fn fill_in_selection_proofs<S: ValidatorStore + 'static, T: SlotClock + 's
|
||||
// Sort duties by slot in a BTreeMap.
|
||||
let mut duties_by_slot: BTreeMap<Slot, Vec<_>> = BTreeMap::new();
|
||||
|
||||
for duty in duties {
|
||||
duties_by_slot.entry(duty.slot).or_default().push(duty);
|
||||
for duty in &duties {
|
||||
duties_by_slot
|
||||
.entry(duty.slot)
|
||||
.or_default()
|
||||
.push(duty.clone());
|
||||
}
|
||||
|
||||
// At halfway through each slot when nothing else is likely to be getting signed, sign a batch
|
||||
// of selection proofs and insert them into the duties service `attesters` map.
|
||||
let slot_clock = &duties_service.slot_clock;
|
||||
|
||||
// Create a HashMap for BeaconCommitteeSelection to match the duty later for distributed case involving middleware
|
||||
let mut selection_hashmap = HashMap::new();
|
||||
let mut call_selection_endpoint = false;
|
||||
|
||||
while !duties_by_slot.is_empty() {
|
||||
if let Some(duration) = slot_clock.duration_to_next_slot() {
|
||||
sleep(
|
||||
@@ -1389,10 +1512,77 @@ async fn fill_in_selection_proofs<S: ValidatorStore + 'static, T: SlotClock + 's
|
||||
&[validator_metrics::ATTESTATION_SELECTION_PROOFS],
|
||||
);
|
||||
|
||||
// In distributed case, we want to send all partial selection proofs to the middleware to determine aggregation duties,
|
||||
// as the middleware will need to have a threshold of partial selection proofs to be able to return the full selection proof
|
||||
// Thus, sign selection proofs in parallel in distributed case; Otherwise, sign them serially in non-distributed (normal) case
|
||||
if duties_service.selection_proof_config.parallel_sign {
|
||||
// for distributed case that uses the selections_endpoint
|
||||
if duties_service.selection_proof_config.selections_endpoint {
|
||||
// Using lookahead_slot to determine if it is the first slot of an epoch
|
||||
let is_lookahead_slot_epoch_start = lookahead_slot % S::E::slots_per_epoch() == 0;
|
||||
|
||||
// Call the selection endpoint only at the first slot of an epoch or when it errors
|
||||
if is_lookahead_slot_epoch_start || call_selection_endpoint {
|
||||
let beacon_committee_selections =
|
||||
make_beacon_committee_selection(&duties_service, &duties).await;
|
||||
|
||||
let selections = match beacon_committee_selections {
|
||||
Ok(selections) => selections,
|
||||
Err(e) => {
|
||||
error!(
|
||||
error = ?e,
|
||||
"Failed to fetch selection proofs"
|
||||
);
|
||||
// If calling the endpoint fails, change to true so that it will retry the next slot
|
||||
call_selection_endpoint = true;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for selection in &selections {
|
||||
// This is a full_selection_proof returned by middleware
|
||||
let selection_proof =
|
||||
SelectionProof::from(selection.selection_proof.clone());
|
||||
selection_hashmap
|
||||
.insert((selection.validator_index, selection.slot), selection_proof);
|
||||
}
|
||||
// Once we have the selection_proof, we don't call the selections_endpoint again
|
||||
call_selection_endpoint = false;
|
||||
}
|
||||
|
||||
for duty in relevant_duties.into_values().flatten() {
|
||||
let key = (duty.validator_index, duty.slot);
|
||||
|
||||
let result = if let Some(selection_proof) = selection_hashmap.remove(&key) {
|
||||
match selection_proof
|
||||
.is_aggregator(duty.committee_length as usize, &duties_service.spec)
|
||||
.map_err(Error::<S::Error>::InvalidModulo)
|
||||
{
|
||||
// Aggregator, return the result
|
||||
Ok(true) => Ok((duty, Some(selection_proof))),
|
||||
// Not an aggregator, do nothing and continue
|
||||
Ok(false) => continue,
|
||||
Err(_) => return,
|
||||
}
|
||||
} else {
|
||||
Err(Error::FailedToProduceSelectionProof(
|
||||
ValidatorStoreError::Middleware(format!(
|
||||
"Missing selection proof for validator {} slot {}",
|
||||
duty.validator_index, duty.slot
|
||||
)),
|
||||
))
|
||||
};
|
||||
|
||||
let mut attesters = duties_service.attesters.write();
|
||||
// if process_duty_and_proof returns false, exit the loop
|
||||
if !process_duty_and_proof::<S>(
|
||||
&mut attesters,
|
||||
result,
|
||||
dependent_root,
|
||||
current_slot,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// For distributed case that uses parallel_sign
|
||||
else if duties_service.selection_proof_config.parallel_sign {
|
||||
let mut duty_and_proof_results = relevant_duties
|
||||
.into_values()
|
||||
.flatten()
|
||||
@@ -1401,8 +1591,6 @@ async fn fill_in_selection_proofs<S: ValidatorStore + 'static, T: SlotClock + 's
|
||||
&duty,
|
||||
duties_service.validator_store.as_ref(),
|
||||
&duties_service.spec,
|
||||
&duties_service.beacon_nodes,
|
||||
&duties_service.selection_proof_config,
|
||||
)
|
||||
.await?;
|
||||
Ok((duty, opt_selection_proof))
|
||||
@@ -1429,8 +1617,6 @@ async fn fill_in_selection_proofs<S: ValidatorStore + 'static, T: SlotClock + 's
|
||||
&duty,
|
||||
duties_service.validator_store.as_ref(),
|
||||
&duties_service.spec,
|
||||
&duties_service.beacon_nodes,
|
||||
&duties_service.selection_proof_config,
|
||||
)
|
||||
.await?;
|
||||
Ok((duty, opt_selection_proof))
|
||||
@@ -1857,6 +2043,209 @@ async fn poll_beacon_proposers<S: ValidatorStore, T: SlotClock + 'static>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query the beacon node for ptc duties for any known validators.
|
||||
async fn poll_beacon_ptc_attesters<S: ValidatorStore + 'static, T: SlotClock + 'static>(
|
||||
duties_service: &Arc<DutiesService<S, T>>,
|
||||
) -> Result<(), Error<S::Error>> {
|
||||
let current_epoch_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::DUTIES_SERVICE_TIMES,
|
||||
&[validator_metrics::UPDATE_PTC_CURRENT_EPOCH],
|
||||
);
|
||||
|
||||
let current_slot = duties_service
|
||||
.slot_clock
|
||||
.now()
|
||||
.ok_or(Error::UnableToReadSlotClock)?;
|
||||
let current_epoch = current_slot.epoch(S::E::slots_per_epoch());
|
||||
|
||||
// Collect *all* pubkeys, even those undergoing 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());
|
||||
|
||||
for &pubkey in &local_pubkeys {
|
||||
if let Some(validator_index) = duties_service.validator_store.validator_index(&pubkey) {
|
||||
local_indices.push(validator_index)
|
||||
}
|
||||
}
|
||||
local_indices
|
||||
};
|
||||
|
||||
// Poll for current epoch
|
||||
if let Err(e) = poll_beacon_ptc_attesters_for_epoch(
|
||||
duties_service,
|
||||
current_epoch,
|
||||
&local_indices,
|
||||
&local_pubkeys,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
%current_epoch,
|
||||
request_epoch = %current_epoch,
|
||||
err = ?e,
|
||||
"Failed to download PTC duties"
|
||||
);
|
||||
}
|
||||
drop(current_epoch_timer);
|
||||
let next_epoch_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::DUTIES_SERVICE_TIMES,
|
||||
&[validator_metrics::UPDATE_PTC_NEXT_EPOCH],
|
||||
);
|
||||
|
||||
// Poll for next epoch
|
||||
let next_epoch = current_epoch + 1;
|
||||
if let Err(e) = poll_beacon_ptc_attesters_for_epoch(
|
||||
duties_service,
|
||||
next_epoch,
|
||||
&local_indices,
|
||||
&local_pubkeys,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
%current_epoch,
|
||||
request_epoch = %next_epoch,
|
||||
err = ?e,
|
||||
"Failed to download PTC duties"
|
||||
);
|
||||
}
|
||||
drop(next_epoch_timer);
|
||||
|
||||
// Prune old duties.
|
||||
duties_service
|
||||
.ptc_duties
|
||||
.write()
|
||||
.retain(|&epoch, _| epoch + HISTORICAL_DUTIES_EPOCHS >= current_epoch);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For the given `local_indices` and `local_pubkeys`, download the PTC duties for the given `epoch` and
|
||||
/// store them in `duties_service.ptc_duties` using bandwidth optimization.
|
||||
async fn poll_beacon_ptc_attesters_for_epoch<
|
||||
S: ValidatorStore + 'static,
|
||||
T: SlotClock + 'static,
|
||||
>(
|
||||
duties_service: &Arc<DutiesService<S, T>>,
|
||||
epoch: Epoch,
|
||||
local_indices: &[u64],
|
||||
local_pubkeys: &HashSet<PublicKeyBytes>,
|
||||
) -> Result<(), Error<S::Error>> {
|
||||
// No need to bother the BN if we don't have any validators.
|
||||
if local_indices.is_empty() {
|
||||
debug!(
|
||||
%epoch,
|
||||
"No validators, not downloading PTC duties"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let fetch_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::DUTIES_SERVICE_TIMES,
|
||||
&[validator_metrics::UPDATE_PTC_FETCH],
|
||||
);
|
||||
|
||||
// TODO(gloas) Unlike attester duties which use `get_uninitialized_validators` to detect
|
||||
// newly-added validators, PTC duties only check dependent_root changes. Validators added
|
||||
// mid-epoch won't get PTC duties until the next epoch boundary. We should probably fix this.
|
||||
let initial_indices_to_request =
|
||||
&local_indices[0..min(INITIAL_PTC_DUTIES_QUERY_SIZE, local_indices.len())];
|
||||
|
||||
let response =
|
||||
post_validator_duties_ptc(duties_service, epoch, initial_indices_to_request).await?;
|
||||
let dependent_root = response.dependent_root;
|
||||
|
||||
// Check if we need to update duties for this epoch and collect validators to update.
|
||||
// We update if we have no epoch data OR if the dependent_root changed.
|
||||
let validators_to_update = {
|
||||
// Avoid holding the read-lock for any longer than required.
|
||||
let ptc_duties = duties_service.ptc_duties.read();
|
||||
let needs_update = ptc_duties.get(&epoch).is_none_or(|(prior_root, _duties)| {
|
||||
// Update if dependent_root changed
|
||||
*prior_root != dependent_root
|
||||
});
|
||||
|
||||
if needs_update {
|
||||
local_pubkeys.iter().collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
if validators_to_update.is_empty() {
|
||||
// No validators have conflicting (epoch, dependent_root) values for this epoch.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Make a request for all indices that require updating which we have not already made a request for.
|
||||
let indices_to_request = validators_to_update
|
||||
.iter()
|
||||
.filter_map(|pubkey| duties_service.validator_store.validator_index(pubkey))
|
||||
.filter(|validator_index| !initial_indices_to_request.contains(validator_index))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Filter the initial duties by their relevance so that we don't hit warnings about
|
||||
// overwriting duties.
|
||||
let new_initial_duties = response
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|duty| validators_to_update.contains(&&duty.pubkey));
|
||||
|
||||
let mut new_duties = if !indices_to_request.is_empty() {
|
||||
post_validator_duties_ptc(duties_service, epoch, indices_to_request.as_slice())
|
||||
.await?
|
||||
.data
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
new_duties.extend(new_initial_duties);
|
||||
|
||||
drop(fetch_timer);
|
||||
|
||||
let _store_timer = validator_metrics::start_timer_vec(
|
||||
&validator_metrics::DUTIES_SERVICE_TIMES,
|
||||
&[validator_metrics::UPDATE_PTC_STORE],
|
||||
);
|
||||
|
||||
debug!(
|
||||
%dependent_root,
|
||||
num_new_duties = new_duties.len(),
|
||||
"Downloaded PTC duties"
|
||||
);
|
||||
|
||||
// Update duties - we only reach here if dependent_root changed or epoch is missing
|
||||
let mut ptc_duties = duties_service.ptc_duties.write();
|
||||
|
||||
match ptc_duties.entry(epoch) {
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
// Dependent root must have changed, so we do complete replacement.
|
||||
// We cannot support partial updates for the same dependent_root.
|
||||
// The beacon node may return incomplete duty lists and we cannot distinguish between "no duties" and
|
||||
// "duties not included in this response". We could query all local validators in each
|
||||
// `post_validator_duties_ptc` call regardless of dependent_root changes, but the bandwidth
|
||||
// cost is likely not justified since PTC assignments are sparse.
|
||||
let (existing_root, _existing_duties) = entry.get();
|
||||
debug!(
|
||||
old_root = %existing_root,
|
||||
new_root = %dependent_root,
|
||||
"PTC dependent root changed, replacing all duties"
|
||||
);
|
||||
|
||||
*entry.get_mut() = (dependent_root, new_duties);
|
||||
}
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
// No existing duties for this epoch
|
||||
entry.insert((dependent_root, new_duties));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify the block service if it should produce a block.
|
||||
async fn notify_block_production_service<S: ValidatorStore>(
|
||||
current_slot: Slot,
|
||||
|
||||
@@ -136,7 +136,7 @@ impl<S, T> Deref for InclusionListService<S, T> {
|
||||
impl<S: ValidatorStore + 'static, T: SlotClock + 'static> InclusionListService<S, T> {
|
||||
/// Starts the service which periodically produces inclusion lists.
|
||||
pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> {
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
let slot_duration = spec.get_slot_duration();
|
||||
|
||||
let duration_to_next_slot = self
|
||||
.slot_clock
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod duties_service;
|
||||
pub mod inclusion_list_service;
|
||||
pub mod latency_service;
|
||||
pub mod notifier_service;
|
||||
pub mod payload_attestation_service;
|
||||
pub mod preparation_service;
|
||||
pub mod sync;
|
||||
pub mod sync_committee_service;
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::duties_service::DutiesService;
|
||||
use slot_clock::SlotClock;
|
||||
use std::sync::Arc;
|
||||
use task_executor::TaskExecutor;
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info};
|
||||
use types::{ChainSpec, EthSpec};
|
||||
use validator_metrics::set_gauge;
|
||||
@@ -14,7 +14,7 @@ pub fn spawn_notifier<S: ValidatorStore + 'static, T: SlotClock + 'static>(
|
||||
executor: TaskExecutor,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), String> {
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
let slot_duration = spec.get_slot_duration();
|
||||
|
||||
let interval_fut = async move {
|
||||
loop {
|
||||
@@ -109,6 +109,7 @@ pub async fn notify<S: ValidatorStore, T: SlotClock + 'static>(
|
||||
let total_validators = duties_service.total_validator_count();
|
||||
let proposing_validators = duties_service.proposer_count(epoch);
|
||||
let attesting_validators = duties_service.attester_count(epoch);
|
||||
let ptc_validators = duties_service.ptc_count(epoch);
|
||||
let doppelganger_detecting_validators = duties_service.doppelganger_detecting_count();
|
||||
|
||||
if doppelganger_detecting_validators > 0 {
|
||||
@@ -126,6 +127,7 @@ pub async fn notify<S: ValidatorStore, T: SlotClock + 'static>(
|
||||
} else if total_validators == attesting_validators {
|
||||
info!(
|
||||
current_epoch_proposers = proposing_validators,
|
||||
current_epoch_ptc = ptc_validators,
|
||||
active_validators = attesting_validators,
|
||||
total_validators = total_validators,
|
||||
%epoch,
|
||||
@@ -135,6 +137,7 @@ pub async fn notify<S: ValidatorStore, T: SlotClock + 'static>(
|
||||
} else if attesting_validators > 0 {
|
||||
info!(
|
||||
current_epoch_proposers = proposing_validators,
|
||||
current_epoch_ptc = ptc_validators,
|
||||
active_validators = attesting_validators,
|
||||
total_validators = total_validators,
|
||||
%epoch,
|
||||
@@ -146,7 +149,7 @@ pub async fn notify<S: ValidatorStore, T: SlotClock + 'static>(
|
||||
validators = total_validators,
|
||||
%epoch,
|
||||
%slot,
|
||||
"Awaiting activation"
|
||||
"All validators inactive"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
use crate::duties_service::DutiesService;
|
||||
use beacon_node_fallback::BeaconNodeFallback;
|
||||
use logging::crit;
|
||||
use slot_clock::SlotClock;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use task_executor::TaskExecutor;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info};
|
||||
use types::{ChainSpec, EthSpec};
|
||||
use validator_store::ValidatorStore;
|
||||
|
||||
pub struct Inner<S, T> {
|
||||
duties_service: Arc<DutiesService<S, T>>,
|
||||
validator_store: Arc<S>,
|
||||
slot_clock: T,
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T>>,
|
||||
executor: TaskExecutor,
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
}
|
||||
|
||||
pub struct PayloadAttestationService<S, T> {
|
||||
inner: Arc<Inner<S, T>>,
|
||||
}
|
||||
|
||||
impl<S, T> Clone for PayloadAttestationService<S, T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> Deref for PayloadAttestationService<S, T> {
|
||||
type Target = Inner<S, T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ValidatorStore + 'static, T: SlotClock + 'static> PayloadAttestationService<S, T> {
|
||||
pub fn new(
|
||||
duties_service: Arc<DutiesService<S, T>>,
|
||||
validator_store: Arc<S>,
|
||||
slot_clock: T,
|
||||
beacon_nodes: Arc<BeaconNodeFallback<T>>,
|
||||
executor: TaskExecutor,
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Inner {
|
||||
duties_service,
|
||||
validator_store,
|
||||
slot_clock,
|
||||
beacon_nodes,
|
||||
executor,
|
||||
chain_spec,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_update_service(self) -> Result<(), String> {
|
||||
let slot_duration = self.chain_spec.get_slot_duration();
|
||||
let payload_attestation_due = self.chain_spec.get_payload_attestation_due();
|
||||
|
||||
info!(
|
||||
payload_attestation_due_ms = payload_attestation_due.as_millis(),
|
||||
"Payload attestation service started"
|
||||
);
|
||||
|
||||
let executor = self.executor.clone();
|
||||
|
||||
let interval_fut = async move {
|
||||
loop {
|
||||
let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else {
|
||||
error!("Failed to read slot clock");
|
||||
sleep(slot_duration).await;
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(current_slot) = self.slot_clock.now() else {
|
||||
error!("Failed to read slot clock after trigger");
|
||||
continue;
|
||||
};
|
||||
|
||||
if !self
|
||||
.chain_spec
|
||||
.fork_name_at_slot::<S::E>(current_slot)
|
||||
.gloas_enabled()
|
||||
{
|
||||
let duration_to_next_epoch = self
|
||||
.slot_clock
|
||||
.duration_to_next_epoch(S::E::slots_per_epoch())
|
||||
.unwrap_or_else(|| {
|
||||
self.chain_spec.get_slot_duration() * S::E::slots_per_epoch() as u32
|
||||
});
|
||||
sleep(duration_to_next_epoch).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
sleep(duration_to_next_slot + payload_attestation_due).await;
|
||||
|
||||
let Some(attestation_slot) = self.slot_clock.now() else {
|
||||
error!("Failed to read slot clock after sleep");
|
||||
continue;
|
||||
};
|
||||
|
||||
let service = self.clone();
|
||||
self.executor.spawn(
|
||||
async move {
|
||||
service.produce_and_publish(attestation_slot).await;
|
||||
},
|
||||
"payload_attestation_producer",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
executor.spawn(interval_fut, "payload_attestation_service");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn produce_and_publish(&self, slot: types::Slot) {
|
||||
let duties = self.duties_service.get_ptc_duties_for_slot(slot);
|
||||
|
||||
if duties.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(
|
||||
%slot,
|
||||
duty_count = duties.len(),
|
||||
"Producing payload attestations"
|
||||
);
|
||||
|
||||
let attestation_data = match self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
beacon_node
|
||||
.get_validator_payload_attestation_data(slot)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get payload attestation data: {e:?}"))
|
||||
.map(|resp| resp.into_data())
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
crit!(
|
||||
error = %e,
|
||||
%slot,
|
||||
"Failed to produce payload attestation data"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
%slot,
|
||||
beacon_block_root = ?attestation_data.beacon_block_root,
|
||||
payload_present = attestation_data.payload_present,
|
||||
"Received payload attestation data"
|
||||
);
|
||||
|
||||
let mut messages = Vec::with_capacity(duties.len());
|
||||
|
||||
for duty in &duties {
|
||||
match self
|
||||
.validator_store
|
||||
.sign_payload_attestation(duty.pubkey, attestation_data.clone())
|
||||
.await
|
||||
{
|
||||
Ok(message) => {
|
||||
messages.push(message);
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
error = ?e,
|
||||
validator = ?duty.pubkey,
|
||||
%slot,
|
||||
"Failed to sign payload attestation"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if messages.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let count = messages.len();
|
||||
let fork_name = self.chain_spec.fork_name_at_slot::<S::E>(slot);
|
||||
let result = self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| {
|
||||
let messages = messages.clone();
|
||||
async move {
|
||||
beacon_node
|
||||
.post_beacon_pool_payload_attestations_ssz(&messages, fork_name)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to publish payload attestations (SSZ): {e:?}"))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let result = match result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => {
|
||||
debug!(%slot, "SSZ publish failed, falling back to JSON");
|
||||
self.beacon_nodes
|
||||
.first_success(|beacon_node| {
|
||||
let messages = messages.clone();
|
||||
async move {
|
||||
beacon_node
|
||||
.post_beacon_pool_payload_attestations(&messages, fork_name)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
format!("Failed to publish payload attestations (JSON): {e:?}")
|
||||
})
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!(
|
||||
%slot,
|
||||
%count,
|
||||
"Successfully published payload attestations"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
error = %e,
|
||||
%slot,
|
||||
"Failed to publish payload attestations"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use task_executor::TaskExecutor;
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use types::{
|
||||
Address, ChainSpec, EthSpec, ProposerPreparationData, SignedValidatorRegistrationData,
|
||||
@@ -174,7 +174,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> PreparationService<S,
|
||||
|
||||
/// Starts the service which periodically produces proposer preparations.
|
||||
pub fn start_proposer_prepare_service(self, spec: &ChainSpec) -> Result<(), String> {
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
let slot_duration = spec.get_slot_duration();
|
||||
info!("Proposer preparation service started");
|
||||
|
||||
let executor = self.executor.clone();
|
||||
@@ -214,7 +214,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> PreparationService<S,
|
||||
info!("Validator registration service started");
|
||||
|
||||
let spec = spec.clone();
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
let slot_duration = spec.get_slot_duration();
|
||||
|
||||
let executor = self.executor.clone();
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::duties_service::DutiesService;
|
||||
use beacon_node_fallback::{ApiTopic, BeaconNodeFallback};
|
||||
use bls::PublicKeyBytes;
|
||||
use eth2::types::BlockId;
|
||||
use futures::StreamExt;
|
||||
use futures::future::FutureExt;
|
||||
use futures::future::join_all;
|
||||
use logging::crit;
|
||||
use slot_clock::SlotClock;
|
||||
use std::collections::HashMap;
|
||||
@@ -17,7 +17,7 @@ use types::{
|
||||
ChainSpec, EthSpec, Hash256, Slot, SyncCommitteeSubscription, SyncContributionData, SyncDuty,
|
||||
SyncSelectionProof, SyncSubnetId,
|
||||
};
|
||||
use validator_store::{Error as ValidatorStoreError, ValidatorStore};
|
||||
use validator_store::{ContributionToSign, SyncMessageToSign, ValidatorStore};
|
||||
|
||||
pub const SUBSCRIPTION_LOOKAHEAD_EPOCHS: u64 = 4;
|
||||
|
||||
@@ -93,7 +93,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let slot_duration = Duration::from_secs(spec.seconds_per_slot);
|
||||
let slot_duration = spec.get_slot_duration();
|
||||
let duration_to_next_slot = self
|
||||
.slot_clock
|
||||
.duration_to_next_slot()
|
||||
@@ -106,18 +106,20 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
|
||||
let executor = self.executor.clone();
|
||||
|
||||
let sync_message_slot_component = spec.get_sync_message_due();
|
||||
|
||||
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.
|
||||
sleep(duration_to_next_slot + slot_duration / 3).await;
|
||||
sleep(duration_to_next_slot + sync_message_slot_component).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 {
|
||||
if let Err(e) = self.spawn_contribution_tasks().await {
|
||||
crit!(
|
||||
error = ?e,
|
||||
"Failed to spawn sync contribution tasks"
|
||||
@@ -140,7 +142,8 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn spawn_contribution_tasks(&self, slot_duration: Duration) -> Result<(), String> {
|
||||
async fn spawn_contribution_tasks(&self) -> Result<(), String> {
|
||||
let spec = &self.duties_service.spec;
|
||||
let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?;
|
||||
let duration_to_next_slot = self
|
||||
.slot_clock
|
||||
@@ -151,7 +154,8 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
// through the slot. This delay triggers at this time
|
||||
let aggregate_production_instant = Instant::now()
|
||||
+ duration_to_next_slot
|
||||
.checked_sub(slot_duration / 3)
|
||||
.checked_add(spec.get_contribution_message_due())
|
||||
.and_then(|offset| offset.checked_sub(spec.get_slot_duration()))
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
let Some(slot_duties) = self
|
||||
@@ -210,7 +214,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
.map(|_| ())
|
||||
.await
|
||||
}
|
||||
.instrument(info_span!("sync_committee_signature_publish", %slot)),
|
||||
.instrument(info_span!("lh_sync_committee_signature_publish", %slot)),
|
||||
"sync_committee_signature_publish",
|
||||
);
|
||||
|
||||
@@ -228,7 +232,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
.map(|_| ())
|
||||
.await
|
||||
}
|
||||
.instrument(info_span!("sync_committee_aggregate_publish", %slot)),
|
||||
.instrument(info_span!("lh_sync_committee_aggregate_publish", %slot)),
|
||||
"sync_committee_aggregate_publish",
|
||||
);
|
||||
|
||||
@@ -243,78 +247,57 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
beacon_block_root: Hash256,
|
||||
validator_duties: Vec<SyncDuty>,
|
||||
) -> Result<(), ()> {
|
||||
// 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!(
|
||||
?pubkey,
|
||||
validator_index = duty.validator_index,
|
||||
%slot,
|
||||
"Missing pubkey for sync committee signature"
|
||||
);
|
||||
None
|
||||
let messages_to_sign: Vec<_> = validator_duties
|
||||
.iter()
|
||||
.map(|duty| SyncMessageToSign {
|
||||
slot,
|
||||
beacon_block_root,
|
||||
validator_index: duty.validator_index,
|
||||
pubkey: duty.pubkey,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let signature_stream = self
|
||||
.validator_store
|
||||
.sign_sync_committee_signatures(messages_to_sign);
|
||||
tokio::pin!(signature_stream);
|
||||
|
||||
while let Some(result) = signature_stream.next().await {
|
||||
match result {
|
||||
Ok(committee_signatures) if !committee_signatures.is_empty() => {
|
||||
let committee_signatures = &committee_signatures;
|
||||
match self
|
||||
.beacon_nodes
|
||||
.request(ApiTopic::SyncCommittee, |beacon_node| async move {
|
||||
beacon_node
|
||||
.post_beacon_pool_sync_committee_signatures(committee_signatures)
|
||||
.await
|
||||
})
|
||||
.instrument(info_span!(
|
||||
"publish_sync_signatures",
|
||||
count = committee_signatures.len()
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(()) => info!(
|
||||
count = committee_signatures.len(),
|
||||
head_block = ?beacon_block_root,
|
||||
%slot,
|
||||
"Successfully published sync committee messages"
|
||||
),
|
||||
Err(e) => error!(
|
||||
%slot,
|
||||
error = %e,
|
||||
"Unable to publish sync committee messages"
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
validator_index = duty.validator_index,
|
||||
%slot,
|
||||
error = ?e,
|
||||
"Failed to sign sync committee signature"
|
||||
);
|
||||
None
|
||||
crit!(%slot, error = ?e, "Failed to sign sync committee signatures");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let committee_signatures = &join_all(signature_futures)
|
||||
.instrument(info_span!(
|
||||
"sign_sync_signatures",
|
||||
count = validator_duties.len()
|
||||
))
|
||||
.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
|
||||
})
|
||||
.instrument(info_span!(
|
||||
"publish_sync_signatures",
|
||||
count = committee_signatures.len()
|
||||
))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
%slot,
|
||||
error = %e,
|
||||
"Unable to publish sync committee messages"
|
||||
);
|
||||
})?;
|
||||
|
||||
info!(
|
||||
count = committee_signatures.len(),
|
||||
head_block = ?beacon_block_root,
|
||||
%slot,
|
||||
"Successfully published sync committee messages"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -341,7 +324,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
.map(|_| ())
|
||||
.await
|
||||
}
|
||||
.instrument(info_span!("publish_sync_committee_aggregate_for_subnet", %slot, ?beacon_block_root, %subnet_id)),
|
||||
.instrument(info_span!("lh_publish_sync_committee_aggregate_for_subnet", %slot, ?beacon_block_root, %subnet_id)),
|
||||
"sync_committee_aggregate_publish_subnet",
|
||||
);
|
||||
}
|
||||
@@ -385,77 +368,61 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> SyncCommitteeService<S
|
||||
})?
|
||||
.data;
|
||||
|
||||
// Create futures to produce signed contributions.
|
||||
let aggregator_count = subnet_aggregators.len();
|
||||
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!(?pubkey, %slot, "Missing pubkey for sync contribution");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
crit!(
|
||||
let contributions_to_sign: Vec<_> = subnet_aggregators
|
||||
.into_iter()
|
||||
.map(
|
||||
|(aggregator_index, aggregator_pk, selection_proof)| ContributionToSign {
|
||||
aggregator_index,
|
||||
aggregator_pubkey: aggregator_pk,
|
||||
contribution: contribution.clone(),
|
||||
selection_proof,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
let contribution_stream = self
|
||||
.validator_store
|
||||
.sign_sync_committee_contributions(contributions_to_sign);
|
||||
tokio::pin!(contribution_stream);
|
||||
|
||||
while let Some(result) = contribution_stream.next().await {
|
||||
match result {
|
||||
Ok(signed_contributions) if !signed_contributions.is_empty() => {
|
||||
let signed_contributions = &signed_contributions;
|
||||
// Publish to the beacon node.
|
||||
match self
|
||||
.beacon_nodes
|
||||
.first_success(|beacon_node| async move {
|
||||
beacon_node
|
||||
.post_validator_contribution_and_proofs(signed_contributions)
|
||||
.await
|
||||
})
|
||||
.instrument(info_span!(
|
||||
"publish_sync_contributions",
|
||||
count = signed_contributions.len()
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(()) => info!(
|
||||
subnet = %subnet_id,
|
||||
beacon_block_root = %beacon_block_root,
|
||||
num_signers = contribution.aggregation_bits.num_set_bits(),
|
||||
%slot,
|
||||
error = ?e,
|
||||
"Unable to sign sync committee contribution"
|
||||
);
|
||||
None
|
||||
"Successfully published sync contributions"
|
||||
),
|
||||
Err(e) => error!(
|
||||
%slot,
|
||||
error = %e,
|
||||
"Unable to publish signed contributions and proofs"
|
||||
),
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Execute all the futures in parallel, collecting any successful results.
|
||||
let signed_contributions = &join_all(signature_futures)
|
||||
.instrument(info_span!(
|
||||
"sign_sync_contributions",
|
||||
count = aggregator_count
|
||||
))
|
||||
.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
|
||||
})
|
||||
.instrument(info_span!(
|
||||
"publish_sync_contributions",
|
||||
count = signed_contributions.len()
|
||||
))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
%slot,
|
||||
error = %e,
|
||||
"Unable to publish signed contributions and proofs"
|
||||
);
|
||||
})?;
|
||||
|
||||
info!(
|
||||
subnet = %subnet_id,
|
||||
beacon_block_root = %beacon_block_root,
|
||||
num_signers = contribution.aggregation_bits.num_set_bits(),
|
||||
%slot,
|
||||
"Successfully published sync contributions"
|
||||
);
|
||||
Err(e) => {
|
||||
crit!(%slot, error = ?e, "Failed to sign sync committee contributions");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user