mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-21 05:44:44 +00:00
## Issue Addressed Closes #3049 This PR updates widely but this replace is safe as `File::options()` is equivelent to `OpenOptions::new()`. ref: https://doc.rust-lang.org/stable/src/std/fs.rs.html#378-380
380 lines
14 KiB
Rust
380 lines
14 KiB
Rust
use clap::ArgMatches;
|
|
use eth2::types::*;
|
|
use eth2::{BeaconNodeHttpClient, Timeouts};
|
|
use log::{error, info};
|
|
use sensitive_url::SensitiveUrl;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
type CommitteePosition = usize;
|
|
type Committee = u64;
|
|
type InclusionDistance = u64;
|
|
type ValidatorIndex = u64;
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
struct UniqueAttestation {
|
|
slot: Slot,
|
|
committee_index: Committee,
|
|
committee_position: CommitteePosition,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
struct ProposerInfo {
|
|
proposer_index: ValidatorIndex,
|
|
graffiti: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct CommitteeInfo {
|
|
number_of_committees: usize,
|
|
validators_per_committee: usize,
|
|
}
|
|
|
|
async fn get_validator_set_len<T: EthSpec>(
|
|
node: &BeaconNodeHttpClient,
|
|
slot: Slot,
|
|
) -> Result<usize, String> {
|
|
let active_validator_set = node
|
|
.get_beacon_states_validators(StateId::Slot(slot), None, None)
|
|
.await
|
|
.map_err(|e| format!("{:?}", e))?
|
|
.ok_or_else(|| "No validators found".to_string())?
|
|
.data;
|
|
Ok(active_validator_set
|
|
.iter()
|
|
.filter(|x| x.status.superstatus() == ValidatorStatus::Active)
|
|
.count())
|
|
}
|
|
|
|
async fn get_block_attestations_set<'a, T: EthSpec>(
|
|
node: &BeaconNodeHttpClient,
|
|
slot: Slot,
|
|
) -> Result<Option<(HashMap<UniqueAttestation, InclusionDistance>, ProposerInfo)>, String> {
|
|
let mut unique_attestations_set: HashMap<UniqueAttestation, InclusionDistance> = HashMap::new();
|
|
|
|
let option_block: Option<ForkVersionedResponse<SignedBeaconBlock<T>>> = node
|
|
.get_beacon_blocks(BlockId::Slot(slot))
|
|
.await
|
|
.map_err(|e| format!("{:?}", e))?;
|
|
|
|
let block = match option_block {
|
|
Some(block) => block.data,
|
|
// No block was proposed for this slot.
|
|
None => return Ok(None),
|
|
};
|
|
|
|
let proposer = ProposerInfo {
|
|
proposer_index: block.message().proposer_index(),
|
|
graffiti: block
|
|
.message()
|
|
.body()
|
|
.graffiti()
|
|
.as_utf8_lossy()
|
|
// Remove commas and apostropes from graffiti to ensure correct CSV format.
|
|
.replace(',', "")
|
|
.replace('"', "")
|
|
.replace('\'', ""),
|
|
};
|
|
|
|
let attestations = block.message().body().attestations();
|
|
|
|
for attestation in attestations.iter() {
|
|
for (position, voted) in attestation.aggregation_bits.iter().enumerate() {
|
|
if voted {
|
|
let unique_attestation = UniqueAttestation {
|
|
slot: attestation.data.slot,
|
|
committee_index: attestation.data.index,
|
|
committee_position: position,
|
|
};
|
|
let inclusion_distance: u64 = slot
|
|
.as_u64()
|
|
.checked_sub(attestation.data.slot.as_u64())
|
|
.ok_or_else(|| "Attestation slot is larger than the block slot".to_string())?;
|
|
unique_attestations_set.insert(unique_attestation, inclusion_distance);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Some((unique_attestations_set, proposer)))
|
|
}
|
|
|
|
async fn get_epoch_committee_data<T: EthSpec>(
|
|
node: &BeaconNodeHttpClient,
|
|
epoch: Epoch,
|
|
) -> Result<(Vec<CommitteeData>, CommitteeInfo), String> {
|
|
let committee_data = node
|
|
.get_beacon_states_committees(
|
|
StateId::Slot(Epoch::start_slot(epoch, T::slots_per_epoch())),
|
|
None,
|
|
None,
|
|
Some(epoch),
|
|
)
|
|
.await
|
|
.map_err(|e| format!("{:?}", e))?
|
|
.ok_or_else(|| "No committees found".to_string())?
|
|
.data;
|
|
|
|
let committee_info = CommitteeInfo {
|
|
number_of_committees: committee_data.len(),
|
|
// FIXME: validators.len() isn't consistent between different committees in the
|
|
// same epoch.
|
|
validators_per_committee: committee_data[0].validators.len(),
|
|
};
|
|
|
|
Ok((committee_data, committee_info))
|
|
}
|
|
|
|
pub async fn run<T: EthSpec>(matches: &ArgMatches<'_>) -> Result<(), String> {
|
|
const SECONDS_PER_SLOT: Duration = Duration::from_secs(12);
|
|
let output_path: PathBuf = clap_utils::parse_required(matches, "output")?;
|
|
let start_epoch: Epoch = clap_utils::parse_required(matches, "start-epoch")?;
|
|
let offline_window: u64 = matches
|
|
.value_of("offline-window")
|
|
.unwrap_or("3")
|
|
.parse()
|
|
.map_err(|e| format!("{:?}", e))?;
|
|
let calculate_offline_vals = offline_window != 0;
|
|
|
|
if start_epoch == 0 {
|
|
return Err("start_epoch cannot be 0.".to_string());
|
|
}
|
|
let initialization_epoch: Epoch = start_epoch - 1;
|
|
let end_epoch: Epoch = clap_utils::parse_required(matches, "end-epoch")?;
|
|
|
|
if end_epoch < start_epoch {
|
|
return Err("start_epoch must be smaller than end_epoch".to_string());
|
|
}
|
|
|
|
let mut available_attestations_set: HashSet<UniqueAttestation> = HashSet::new();
|
|
let mut included_attestations_set: HashMap<UniqueAttestation, InclusionDistance> =
|
|
HashMap::new();
|
|
|
|
// Build validator set HashMap<Index, EpochOfMostRecentAttestation)>
|
|
// This allows a 'best effort' attempt to normalize block efficiencies.
|
|
let mut online_validator_set: HashMap<ValidatorIndex, Epoch> = HashMap::new();
|
|
|
|
let mut proposer_map: HashMap<Slot, ProposerInfo> = HashMap::new();
|
|
|
|
let mut file = File::options()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.open(output_path)
|
|
.map_err(|e| format!("Unable to open file: {}", e))?;
|
|
|
|
write!(file, "slot,proposer,available,included,offline,graffiti").unwrap();
|
|
|
|
// Initialize API.
|
|
let endpoint = matches
|
|
.value_of("endpoint")
|
|
.unwrap_or("http://localhost:5052/");
|
|
let node = BeaconNodeHttpClient::new(
|
|
SensitiveUrl::parse(endpoint).map_err(|_| "Unable to parse endpoint.".to_string())?,
|
|
Timeouts::set_all(SECONDS_PER_SLOT),
|
|
);
|
|
|
|
// Check we can connect to the API.
|
|
let version =
|
|
match node.get_node_version().await {
|
|
Ok(version_response) => version_response.data.version,
|
|
Err(_) => return Err(
|
|
"Error: A working HTTP API server is required. Ensure one is synced and available."
|
|
.to_string(),
|
|
),
|
|
};
|
|
|
|
// Check we are synced past the required epoch range.
|
|
let head_slot_synced =
|
|
match node.get_node_syncing().await {
|
|
Ok(synced_response) => synced_response.data.head_slot,
|
|
Err(_) => return Err(
|
|
"Error: A working HTTP API server is required. Ensure one is synced and available."
|
|
.to_string(),
|
|
),
|
|
};
|
|
|
|
if head_slot_synced < end_epoch.end_slot(T::slots_per_epoch()) {
|
|
return Err(
|
|
"Error: The beacon node is not sufficiently synced. Make sure your node is synced \
|
|
past the desired `end-epoch` and that you aren't requesting future epochs."
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
// Whether the beacon node is responding to requests. This is helpful for logging.
|
|
let mut connected: bool = true;
|
|
info!("Connected to endpoint at: {:?} - {:?}", endpoint, version);
|
|
|
|
// Loop over epochs.
|
|
for epoch in (initialization_epoch.as_u64()..=end_epoch.as_u64()).map(Epoch::new) {
|
|
if epoch != initialization_epoch {
|
|
info!("Analysing epoch {}...", epoch);
|
|
} else {
|
|
info!("Initializing...");
|
|
}
|
|
let mut epoch_data: Vec<(Slot, Option<ProposerInfo>, usize, usize)> = Vec::new();
|
|
|
|
// Current epochs available attestations set
|
|
let (committee_data, committee_info) = loop {
|
|
if let Ok(committee_result) = get_epoch_committee_data::<T>(&node, epoch).await {
|
|
if !connected {
|
|
info!("Connected to endpoint at: {:?} - {:?}", endpoint, version);
|
|
connected = true;
|
|
}
|
|
break committee_result;
|
|
}
|
|
|
|
if connected {
|
|
connected = false;
|
|
error!("A request to the Beacon Node API failed. Check connectivity.");
|
|
}
|
|
};
|
|
|
|
// Ensure available attestations don't exceed the possible amount of attestations
|
|
// as determined by the committee size/number.
|
|
// This is unlikely to happen, but not impossible.
|
|
let max_possible_attesations =
|
|
committee_info.validators_per_committee * committee_info.number_of_committees;
|
|
|
|
// Get number of active validators.
|
|
let active_validators =
|
|
get_validator_set_len::<T>(&node, epoch.start_slot(T::slots_per_epoch())).await?;
|
|
|
|
for slot in epoch.slot_iter(T::slots_per_epoch()) {
|
|
// Get all included attestations.
|
|
let block_result = loop {
|
|
if let Ok(block_result) = get_block_attestations_set::<T>(&node, slot).await {
|
|
if !connected {
|
|
info!("Connected to endpoint at: {:?} - {:?}", endpoint, version);
|
|
connected = true;
|
|
}
|
|
break block_result;
|
|
};
|
|
if connected {
|
|
connected = false;
|
|
error!("A request to the Beacon Node API failed. Check connectivity.");
|
|
}
|
|
};
|
|
let (mut attestations_in_block, proposer) = match block_result {
|
|
Some(output) => (output.0, Some(output.1)),
|
|
None => (HashMap::new(), None),
|
|
};
|
|
|
|
// Insert block proposer into proposer_map.
|
|
if let Some(proposer_info) = proposer {
|
|
proposer_map.insert(slot, proposer_info.clone());
|
|
}
|
|
|
|
// Remove duplicate attestations.
|
|
attestations_in_block.retain(|x, _| included_attestations_set.get(x).is_none());
|
|
|
|
// Add them to the set.
|
|
included_attestations_set.extend(attestations_in_block.clone());
|
|
|
|
// Remove expired available attestations.
|
|
available_attestations_set.retain(|x| x.slot >= (slot.as_u64().saturating_sub(32)));
|
|
|
|
// Don't write data from the initialization epoch.
|
|
if epoch != initialization_epoch {
|
|
let included = attestations_in_block.len();
|
|
|
|
let available = if max_possible_attesations < available_attestations_set.len() {
|
|
max_possible_attesations
|
|
} else {
|
|
available_attestations_set.len()
|
|
};
|
|
|
|
// Get proposer information.
|
|
let proposer = proposer_map.get(&slot).cloned();
|
|
|
|
// Store slot data.
|
|
epoch_data.push((slot, proposer, available, included));
|
|
}
|
|
|
|
// Included attestations are no longer available.
|
|
for new_attestation in &attestations_in_block {
|
|
available_attestations_set.remove(new_attestation.0);
|
|
}
|
|
|
|
// Get all available attestations.
|
|
for committee in &committee_data {
|
|
if committee.slot == slot {
|
|
for position in 0..committee.validators.len() {
|
|
let unique_attestation = UniqueAttestation {
|
|
slot: committee.slot,
|
|
committee_index: committee.index,
|
|
committee_position: position,
|
|
};
|
|
available_attestations_set.insert(unique_attestation.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut offline = "None".to_string();
|
|
if calculate_offline_vals {
|
|
// Get all online validators for the epoch.
|
|
for committee in &committee_data {
|
|
for position in 0..committee.validators.len() {
|
|
let unique_attestation = UniqueAttestation {
|
|
slot: committee.slot,
|
|
committee_index: committee.index,
|
|
committee_position: position,
|
|
};
|
|
let index = committee.validators.get(position).ok_or_else(|| {
|
|
"Error parsing validator indices from committee data".to_string()
|
|
})?;
|
|
|
|
if included_attestations_set.get(&unique_attestation).is_some() {
|
|
online_validator_set.insert(*index, epoch);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate offline validators.
|
|
offline = if epoch >= start_epoch + offline_window {
|
|
active_validators
|
|
.checked_sub(online_validator_set.len())
|
|
.ok_or_else(|| "Online set is greater than active set".to_string())?
|
|
.to_string()
|
|
} else {
|
|
"None".to_string()
|
|
};
|
|
}
|
|
|
|
// Write epoch data.
|
|
for (slot, proposer, available, included) in epoch_data {
|
|
let proposer_index = proposer
|
|
.clone()
|
|
.map_or("None".to_string(), |x| x.proposer_index.to_string());
|
|
let graffiti = proposer.map_or("None".to_string(), |x| x.graffiti);
|
|
write!(
|
|
file,
|
|
"\n{},{},{},{},{},{}",
|
|
slot, proposer_index, available, included, offline, graffiti
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
// Free some memory by removing included attestations older than 1 epoch.
|
|
included_attestations_set.retain(|x, _| {
|
|
x.slot >= Epoch::new(epoch.as_u64().saturating_sub(1)).start_slot(T::slots_per_epoch())
|
|
});
|
|
|
|
if calculate_offline_vals {
|
|
// Remove old validators from the validator set which are outside the offline window.
|
|
online_validator_set.retain(|_, x| {
|
|
*x >= Epoch::new(
|
|
epoch
|
|
.as_u64()
|
|
.saturating_sub(offline_window.saturating_sub(1)),
|
|
)
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|