resolve merge conflicts

This commit is contained in:
Eitan Seri-Levi
2026-04-30 01:51:26 +02:00
544 changed files with 48684 additions and 18351 deletions

View File

@@ -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");
}
_ => {}
}
}

View File

@@ -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(())
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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"
);
}
}
}
}

View File

@@ -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();

View File

@@ -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(())
}