mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-26 09:13:41 +00:00
Process envelopes correctly
This commit is contained in:
@@ -2951,12 +2951,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
};
|
||||
|
||||
// Import the blocks into the chain.
|
||||
for signature_verified_block in signature_verified_blocks {
|
||||
// Import the blocks (and envelopes for Gloas) into the chain.
|
||||
for (signature_verified_block, maybe_envelope) in signature_verified_blocks {
|
||||
let block_root = signature_verified_block.block_root();
|
||||
let block_slot = signature_verified_block.slot();
|
||||
match self
|
||||
.process_block(
|
||||
signature_verified_block.block_root(),
|
||||
block_root,
|
||||
signature_verified_block,
|
||||
notify_execution_layer,
|
||||
BlockImportSource::RangeSync,
|
||||
@@ -2969,6 +2970,22 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
AvailabilityProcessingStatus::Imported(block_root) => {
|
||||
// The block was imported successfully.
|
||||
imported_blocks.push((block_root, block_slot));
|
||||
|
||||
// Gloas: process the envelope now that the block is in fork choice.
|
||||
if let Some(envelope) = maybe_envelope
|
||||
&& let Err(error) = self
|
||||
.process_range_sync_envelope(
|
||||
block_root,
|
||||
envelope,
|
||||
notify_execution_layer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return ChainSegmentResult::Failed {
|
||||
imported_blocks,
|
||||
error: BlockError::EnvelopeError(Box::new(error)),
|
||||
};
|
||||
}
|
||||
}
|
||||
AvailabilityProcessingStatus::MissingComponents(slot, block_root) => {
|
||||
warn!(
|
||||
@@ -7219,7 +7236,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
block_data: AvailableBlockData<T::EthSpec>,
|
||||
) -> Option<StoreOp<'_, T::EthSpec>> {
|
||||
match block_data {
|
||||
AvailableBlockData::NoData => None,
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => None,
|
||||
AvailableBlockData::Blobs(blobs) => {
|
||||
debug!(
|
||||
%block_root,
|
||||
|
||||
@@ -60,6 +60,7 @@ use crate::execution_payload::{
|
||||
};
|
||||
use crate::kzg_utils::blobs_to_data_column_sidecars;
|
||||
use crate::observed_block_producers::SeenBlock;
|
||||
use crate::payload_envelope_verification::{AvailableEnvelope, EnvelopeError};
|
||||
use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS;
|
||||
use crate::validator_pubkey_cache::ValidatorPubkeyCache;
|
||||
use crate::{
|
||||
@@ -328,6 +329,8 @@ pub enum BlockError {
|
||||
/// It's unclear if this block is valid, but it cannot be fully verified without the parent's
|
||||
/// execution payload envelope.
|
||||
ParentEnvelopeUnknown { parent_root: Hash256 },
|
||||
/// An error occurred while processing the execution payload envelope during range sync.
|
||||
EnvelopeError(Box<EnvelopeError>),
|
||||
}
|
||||
|
||||
/// Which specific signature(s) are invalid in a SignedBeaconBlock
|
||||
@@ -591,10 +594,17 @@ pub(crate) fn process_block_slash_info<T: BeaconChainTypes, TErr: BlockBlobError
|
||||
/// The given `chain_segment` must contain only blocks from the same epoch, otherwise an error
|
||||
/// will be returned.
|
||||
#[instrument(skip_all)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
|
||||
mut chain_segment: Vec<(Hash256, RangeSyncBlock<T::EthSpec>)>,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<Vec<SignatureVerifiedBlock<T>>, BlockError> {
|
||||
) -> Result<
|
||||
Vec<(
|
||||
SignatureVerifiedBlock<T>,
|
||||
Option<Box<AvailableEnvelope<T::EthSpec>>>,
|
||||
)>,
|
||||
BlockError,
|
||||
> {
|
||||
if chain_segment.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
@@ -623,14 +633,30 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
|
||||
let consensus_context =
|
||||
ConsensusContext::new(block.slot()).set_current_block_root(block_root);
|
||||
|
||||
let available_block = block.into_available_block();
|
||||
let (available_block, envelope) = match block {
|
||||
RangeSyncBlock::Base(ab) => (ab, None),
|
||||
RangeSyncBlock::Gloas { block, envelope } => {
|
||||
let ab = AvailableBlock::new(
|
||||
block,
|
||||
AvailableBlockData::DataInEnvelope,
|
||||
&chain.data_availability_checker,
|
||||
chain.spec.clone(),
|
||||
)
|
||||
.map_err(BlockError::AvailabilityCheck)?;
|
||||
(ab, envelope)
|
||||
}
|
||||
};
|
||||
|
||||
available_blocks.push(available_block.clone());
|
||||
signature_verified_blocks.push(SignatureVerifiedBlock {
|
||||
block: MaybeAvailableBlock::Available(available_block),
|
||||
block_root,
|
||||
parent: None,
|
||||
consensus_context,
|
||||
});
|
||||
signature_verified_blocks.push((
|
||||
SignatureVerifiedBlock {
|
||||
block: MaybeAvailableBlock::Available(available_block),
|
||||
block_root,
|
||||
parent: None,
|
||||
consensus_context,
|
||||
},
|
||||
envelope,
|
||||
));
|
||||
}
|
||||
|
||||
chain
|
||||
@@ -640,7 +666,7 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
|
||||
// verify signatures
|
||||
let pubkey_cache = get_validator_pubkey_cache(chain)?;
|
||||
let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec);
|
||||
for svb in &mut signature_verified_blocks {
|
||||
for (svb, _) in &mut signature_verified_blocks {
|
||||
signature_verifier
|
||||
.include_all_signatures(svb.block.as_block(), &mut svb.consensus_context)?;
|
||||
}
|
||||
@@ -651,7 +677,7 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
|
||||
|
||||
drop(pubkey_cache);
|
||||
|
||||
if let Some(signature_verified_block) = signature_verified_blocks.first_mut() {
|
||||
if let Some((signature_verified_block, _)) = signature_verified_blocks.first_mut() {
|
||||
signature_verified_block.parent = Some(parent);
|
||||
}
|
||||
|
||||
@@ -1199,7 +1225,7 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> {
|
||||
let result = info_span!("signature_verify").in_scope(|| signature_verifier.verify());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
// gloas blocks are always available.
|
||||
// Gloas blocks are always available — data arrives via the envelope.
|
||||
let maybe_available = if chain
|
||||
.spec
|
||||
.fork_name_at_slot::<T::EthSpec>(block.slot())
|
||||
@@ -1208,7 +1234,7 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> {
|
||||
MaybeAvailableBlock::Available(
|
||||
AvailableBlock::new(
|
||||
block,
|
||||
AvailableBlockData::NoData,
|
||||
AvailableBlockData::DataInEnvelope,
|
||||
&chain.data_availability_checker,
|
||||
chain.spec.clone(),
|
||||
)
|
||||
|
||||
@@ -139,7 +139,9 @@ impl<E: EthSpec> RangeSyncBlock<E> {
|
||||
pub fn n_blobs(&self) -> usize {
|
||||
match self {
|
||||
Self::Base(block) => match block.data() {
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0,
|
||||
AvailableBlockData::NoData
|
||||
| AvailableBlockData::DataInEnvelope
|
||||
| AvailableBlockData::DataColumns(_) => 0,
|
||||
AvailableBlockData::Blobs(blobs) => blobs.len(),
|
||||
},
|
||||
Self::Gloas { .. } => 0,
|
||||
@@ -149,7 +151,9 @@ impl<E: EthSpec> RangeSyncBlock<E> {
|
||||
pub fn n_data_columns(&self) -> usize {
|
||||
match self {
|
||||
Self::Base(block) => match block.data() {
|
||||
AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0,
|
||||
AvailableBlockData::NoData
|
||||
| AvailableBlockData::DataInEnvelope
|
||||
| AvailableBlockData::Blobs(_) => 0,
|
||||
AvailableBlockData::DataColumns(columns) => columns.len(),
|
||||
},
|
||||
Self::Gloas { .. } => 0,
|
||||
|
||||
@@ -366,7 +366,7 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
available_block: &AvailableBlock<T::EthSpec>,
|
||||
) -> Result<(), AvailabilityCheckError> {
|
||||
match available_block.data() {
|
||||
AvailableBlockData::NoData => Ok(()),
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => Ok(()),
|
||||
AvailableBlockData::Blobs(blobs) => verify_kzg_for_blob_list(blobs.iter(), &self.kzg)
|
||||
.map_err(AvailabilityCheckError::InvalidBlobs),
|
||||
AvailableBlockData::DataColumns(columns) => {
|
||||
@@ -388,7 +388,7 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
|
||||
for available_block in available_blocks {
|
||||
match available_block.data().to_owned() {
|
||||
AvailableBlockData::NoData => {}
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => {}
|
||||
AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs),
|
||||
AvailableBlockData::DataColumns(columns) => all_data_columns.extend(columns),
|
||||
}
|
||||
@@ -655,6 +655,9 @@ pub enum AvailableBlockData<E: EthSpec> {
|
||||
Blobs(BlobSidecarList<E>),
|
||||
/// Block is post-PeerDAS and has more than zero blobs
|
||||
DataColumns(DataColumnSidecarList<E>),
|
||||
/// Gloas: block data (payload + columns) arrives via the execution payload envelope,
|
||||
/// not the block itself.
|
||||
DataInEnvelope,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> AvailableBlockData<E> {
|
||||
@@ -676,7 +679,7 @@ impl<E: EthSpec> AvailableBlockData<E> {
|
||||
|
||||
pub fn blobs(&self) -> Option<BlobSidecarList<E>> {
|
||||
match self {
|
||||
AvailableBlockData::NoData => None,
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => None,
|
||||
AvailableBlockData::Blobs(blobs) => Some(blobs.clone()),
|
||||
AvailableBlockData::DataColumns(_) => None,
|
||||
}
|
||||
@@ -692,7 +695,7 @@ impl<E: EthSpec> AvailableBlockData<E> {
|
||||
|
||||
pub fn data_columns(&self) -> Option<DataColumnSidecarList<E>> {
|
||||
match self {
|
||||
AvailableBlockData::NoData => None,
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => None,
|
||||
AvailableBlockData::Blobs(_) => None,
|
||||
AvailableBlockData::DataColumns(data_columns) => Some(data_columns.clone()),
|
||||
}
|
||||
@@ -757,6 +760,8 @@ impl<E: EthSpec> AvailableBlock<E> {
|
||||
return Err(AvailabilityCheckError::MissingBlobs);
|
||||
}
|
||||
}
|
||||
// Gloas: data availability is handled by the envelope path, not the block.
|
||||
AvailableBlockData::DataInEnvelope => {}
|
||||
AvailableBlockData::Blobs(blobs) => {
|
||||
if !blobs_required {
|
||||
return Err(AvailabilityCheckError::InvalidAvailableBlockData);
|
||||
@@ -835,7 +840,7 @@ impl<E: EthSpec> AvailableBlock<E> {
|
||||
|
||||
pub fn has_blobs(&self) -> bool {
|
||||
match self.blob_data {
|
||||
AvailableBlockData::NoData => false,
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => false,
|
||||
AvailableBlockData::Blobs(..) => true,
|
||||
AvailableBlockData::DataColumns(_) => false,
|
||||
}
|
||||
@@ -863,6 +868,7 @@ impl<E: EthSpec> AvailableBlock<E> {
|
||||
AvailableBlockData::DataColumns(data_columns) => {
|
||||
AvailableBlockData::DataColumns(data_columns.clone())
|
||||
}
|
||||
AvailableBlockData::DataInEnvelope => AvailableBlockData::DataInEnvelope,
|
||||
},
|
||||
blobs_available_timestamp: self.blobs_available_timestamp,
|
||||
spec: self.spec.clone(),
|
||||
|
||||
@@ -275,7 +275,7 @@ impl<E: EthSpec> PendingComponents<E> {
|
||||
// Block is available, construct `AvailableExecutedBlock`
|
||||
|
||||
let blobs_available_timestamp = match blob_data {
|
||||
AvailableBlockData::NoData => None,
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => None,
|
||||
AvailableBlockData::Blobs(_) => self
|
||||
.verified_blobs
|
||||
.iter()
|
||||
|
||||
@@ -138,7 +138,7 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
|
||||
};
|
||||
|
||||
let (blobs, data_columns) = match block.data() {
|
||||
AvailableBlockData::NoData => (None, None),
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => (None, None),
|
||||
AvailableBlockData::Blobs(blobs) => (Some(blobs.clone()), None),
|
||||
AvailableBlockData::DataColumns(data_columns) => (None, Some(data_columns.clone())),
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
|
||||
match &block_data {
|
||||
AvailableBlockData::NoData => (),
|
||||
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => (),
|
||||
AvailableBlockData::Blobs(_) => new_oldest_blob_slot = Some(block.slot()),
|
||||
AvailableBlockData::DataColumns(_) => {
|
||||
new_oldest_data_column_slot = Some(block.slot())
|
||||
|
||||
@@ -6,16 +6,25 @@ use fork_choice::PayloadVerificationStatus;
|
||||
use slot_clock::SlotClock;
|
||||
use store::StoreOp;
|
||||
use tracing::{debug, error, info, info_span, instrument, warn};
|
||||
use types::{BeaconState, BlockImportSource, Hash256, Slot};
|
||||
use types::{BeaconState, BlockImportSource, EthSpec, Hash256, Slot};
|
||||
|
||||
use state_processing::{
|
||||
VerifySignatures,
|
||||
envelope_processing::{VerifyStateRoot, process_execution_payload_envelope},
|
||||
};
|
||||
use store::DatabaseBlock;
|
||||
|
||||
use super::{
|
||||
AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, EnvelopeImportData,
|
||||
ExecutedEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope,
|
||||
ExecutedEnvelope, MaybeAvailableEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope,
|
||||
gossip_verified_envelope::verify_envelope_consistency, load_snapshot_from_state_root,
|
||||
payload_notifier::PayloadNotifier,
|
||||
};
|
||||
use crate::{
|
||||
AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes,
|
||||
NotifyExecutionLayer, block_verification_types::AvailableBlockData, metrics,
|
||||
payload_envelope_verification::ExecutionPendingEnvelope, validator_monitor::get_slot_delay_ms,
|
||||
NotifyExecutionLayer, PayloadVerificationOutcome, block_verification_types::AvailableBlockData,
|
||||
metrics, payload_envelope_verification::ExecutionPendingEnvelope,
|
||||
validator_monitor::get_slot_delay_ms,
|
||||
};
|
||||
|
||||
const ENVELOPE_METRICS_CACHE_SLOT_LIMIT: u32 = 64;
|
||||
@@ -148,6 +157,125 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an `AvailableEnvelope` from range sync. Unlike the gossip path, the block has
|
||||
/// already been imported into fork choice so we can skip gossip-specific checks.
|
||||
///
|
||||
/// Steps: consistency checks, signature verification, EL verification (newPayload),
|
||||
/// state processing, await EL result, and import.
|
||||
#[instrument(skip_all, fields(block_root = ?block_root))]
|
||||
pub async fn process_range_sync_envelope(
|
||||
self: &Arc<Self>,
|
||||
block_root: Hash256,
|
||||
available_envelope: Box<AvailableEnvelope<T::EthSpec>>,
|
||||
notify_execution_layer: NotifyExecutionLayer,
|
||||
) -> Result<AvailabilityProcessingStatus, EnvelopeError> {
|
||||
let signed_envelope = available_envelope.envelope().clone();
|
||||
let block_slot = signed_envelope.slot();
|
||||
|
||||
// Load the block from store (just imported, guaranteed to exist).
|
||||
let block = match self.store.try_get_full_block(&block_root)? {
|
||||
Some(DatabaseBlock::Full(block)) => Arc::new(block),
|
||||
Some(DatabaseBlock::Blinded(_)) | None => {
|
||||
return Err(EnvelopeError::BlockRootUnknown { block_root });
|
||||
}
|
||||
};
|
||||
|
||||
// Envelope consistency checks.
|
||||
let execution_bid = &block
|
||||
.message()
|
||||
.body()
|
||||
.signed_execution_payload_bid()?
|
||||
.message;
|
||||
let latest_finalized_slot = self
|
||||
.canonical_head
|
||||
.cached_head()
|
||||
.finalized_checkpoint()
|
||||
.epoch
|
||||
.start_slot(T::EthSpec::slots_per_epoch());
|
||||
verify_envelope_consistency(
|
||||
&signed_envelope.message,
|
||||
&block,
|
||||
execution_bid,
|
||||
latest_finalized_slot,
|
||||
)?;
|
||||
|
||||
// Load state for signature verification and state processing.
|
||||
let snapshot =
|
||||
load_snapshot_from_state_root::<T>(block_root, block.state_root(), &self.store)?;
|
||||
|
||||
// Verify the envelope signature.
|
||||
let is_valid =
|
||||
signed_envelope.verify_signature_with_state(&snapshot.pre_state, &self.spec)?;
|
||||
if !is_valid {
|
||||
return Err(EnvelopeError::BadSignature);
|
||||
}
|
||||
|
||||
// Start EL verification (newPayload) as early as possible.
|
||||
let payload_notifier = PayloadNotifier::new(
|
||||
self.clone(),
|
||||
signed_envelope.clone(),
|
||||
block.clone(),
|
||||
notify_execution_layer,
|
||||
)?;
|
||||
let payload_verification_future = async move {
|
||||
let chain = payload_notifier.chain.clone();
|
||||
if let Some(started_execution) = chain.slot_clock.now_duration() {
|
||||
chain
|
||||
.envelope_times_cache
|
||||
.write()
|
||||
.set_time_started_execution(block_root, block_slot, started_execution);
|
||||
}
|
||||
let payload_verification_status = payload_notifier.notify_new_payload().await?;
|
||||
Ok(PayloadVerificationOutcome {
|
||||
payload_verification_status,
|
||||
})
|
||||
};
|
||||
let payload_verification_handle = self
|
||||
.task_executor
|
||||
.spawn_handle(
|
||||
payload_verification_future,
|
||||
"range_sync_envelope_payload_verification",
|
||||
)
|
||||
.ok_or(BeaconChainError::RuntimeShutdown)?;
|
||||
|
||||
// Run state processing (signatures already verified above).
|
||||
let mut state = snapshot.pre_state;
|
||||
process_execution_payload_envelope(
|
||||
&mut state,
|
||||
Some(snapshot.state_root),
|
||||
&signed_envelope,
|
||||
VerifySignatures::False,
|
||||
VerifyStateRoot::True,
|
||||
&self.spec,
|
||||
)?;
|
||||
|
||||
// Build the ExecutionPendingEnvelope with Available status (columns already bundled).
|
||||
let execution_pending = ExecutionPendingEnvelope {
|
||||
signed_envelope: MaybeAvailableEnvelope::Available(*available_envelope),
|
||||
import_data: EnvelopeImportData {
|
||||
block_root,
|
||||
post_state: Box::new(state),
|
||||
},
|
||||
payload_verification_handle,
|
||||
};
|
||||
|
||||
// Await EL verification and import.
|
||||
let executed_envelope = self
|
||||
.clone()
|
||||
.into_executed_payload_envelope(execution_pending)
|
||||
.await?;
|
||||
|
||||
match executed_envelope {
|
||||
ExecutedEnvelope::Available(envelope) => {
|
||||
self.import_available_execution_payload_envelope(Box::new(envelope))
|
||||
.await
|
||||
}
|
||||
ExecutedEnvelope::AvailabilityPending() => Err(EnvelopeError::InternalError(
|
||||
"Pending payload envelope not yet implemented".to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepts a fully-verified payload envelope and awaits on its payload verification handle to
|
||||
/// get a fully `ExecutedEnvelope`.
|
||||
///
|
||||
|
||||
@@ -75,6 +75,10 @@ impl<E: EthSpec> AvailableEnvelope<E> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn envelope(&self) -> &Arc<SignedExecutionPayloadEnvelope<E>> {
|
||||
&self.envelope
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &ExecutionPayloadEnvelope<E> {
|
||||
&self.envelope.message
|
||||
}
|
||||
|
||||
@@ -1390,7 +1390,10 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
return None;
|
||||
}
|
||||
// BlobNotRequired is unreachable. Only constructed in `process_gossip_blob`
|
||||
Err(e @ BlockError::InternalError(_)) | Err(e @ BlockError::BlobNotRequired(_)) => {
|
||||
// EnvelopeError is unreachable. Only constructed during range sync envelope processing.
|
||||
Err(e @ BlockError::InternalError(_))
|
||||
| Err(e @ BlockError::BlobNotRequired(_))
|
||||
| Err(e @ BlockError::EnvelopeError(_)) => {
|
||||
error!(error = %e, "Internal block gossip validation error");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -619,15 +619,6 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO(gloas): Implement Gloas chain segment processing.
|
||||
// Gloas blocks carry separate envelopes and need a different import path.
|
||||
if downloaded_blocks
|
||||
.iter()
|
||||
.any(|b| matches!(b, RangeSyncBlock::Gloas { .. }))
|
||||
{
|
||||
todo!("Gloas chain segment processing");
|
||||
}
|
||||
|
||||
let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64());
|
||||
let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64());
|
||||
let sent_blocks = downloaded_blocks.len();
|
||||
|
||||
Reference in New Issue
Block a user