Transition block lookup sync to range sync (#6122)

* Transition block lookup sync to range sync

* Log unexpected state

* Merge remote-tracking branch 'sigp/unstable' into lookup-to-range

* Add docs

* Merge remote-tracking branch 'sigp/unstable' into lookup-to-range
This commit is contained in:
Lion - dapplion
2024-10-09 00:18:41 +03:00
committed by GitHub
parent 48dd3f385c
commit 71c5388461
8 changed files with 206 additions and 30 deletions

View File

@@ -28,6 +28,7 @@ use super::network_context::{PeerGroup, RpcResponseError, SyncNetworkContext};
use crate::metrics;
use crate::sync::block_lookups::common::ResponseType;
use crate::sync::block_lookups::parent_chain::find_oldest_fork_ancestor;
use crate::sync::SyncMessage;
use beacon_chain::block_verification_types::AsBlock;
use beacon_chain::data_availability_checker::{
AvailabilityCheckError, AvailabilityCheckErrorCategory,
@@ -55,7 +56,10 @@ mod tests;
/// The maximum depth we will search for a parent block. In principle we should have sync'd any
/// canonical chain to its head once the peer connects. A chain should not appear where it's depth
/// is further back than the most recent head slot.
pub(crate) const PARENT_DEPTH_TOLERANCE: usize = SLOT_IMPORT_TOLERANCE * 2;
///
/// Have the same value as range's sync tolerance to consider a peer synced. Once sync lookup
/// reaches the maximum depth it will force trigger range sync.
pub(crate) const PARENT_DEPTH_TOLERANCE: usize = SLOT_IMPORT_TOLERANCE;
const FAILED_CHAINS_CACHE_EXPIRY_SECONDS: u64 = 60;
pub const SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS: u8 = 4;
@@ -254,22 +258,59 @@ impl<T: BeaconChainTypes> BlockLookups<T> {
// blocks on top of A forming A -> C. The malicious peer forces us to fetch C
// from it, which will result in parent A hitting the chain_too_long error. Then
// the valid chain A -> B is dropped too.
if let Ok(block_to_drop) = find_oldest_fork_ancestor(parent_chains, chain_idx) {
// Drop all lookups descending from the child of the too long parent chain
if let Some((lookup_id, lookup)) = self
//
// `find_oldest_fork_ancestor` should never return Err, unwrapping to tip for
// complete-ness
let parent_chain_tip = parent_chain.tip;
let block_to_drop =
find_oldest_fork_ancestor(parent_chains, chain_idx).unwrap_or(parent_chain_tip);
// Drop all lookups descending from the child of the too long parent chain
if let Some((lookup_id, lookup)) = self
.single_block_lookups
.iter()
.find(|(_, l)| l.block_root() == block_to_drop)
{
// If a lookup chain is too long, we can't distinguish a valid chain from a
// malicious one. We must attempt to sync this chain to not lose liveness. If
// the chain grows too long, we stop lookup sync and transition this head to
// forward range sync. We need to tell range sync which head to sync to, and
// from which peers. The lookup of the very tip of this chain may contain zero
// peers if it's the parent-child lookup. So we do a bit of a trick here:
// - Tell range sync to sync to the tip's root (if available, else its ancestor)
// - But use all peers in the ancestor lookup, which should have at least one
// peer, and its peer set is a strict superset of the tip's lookup.
if let Some((_, tip_lookup)) = self
.single_block_lookups
.iter()
.find(|(_, l)| l.block_root() == block_to_drop)
.find(|(_, l)| l.block_root() == parent_chain_tip)
{
for &peer_id in lookup.all_peers() {
cx.report_peer(
peer_id,
PeerAction::LowToleranceError,
"chain_too_long",
);
}
self.drop_lookup_and_children(*lookup_id);
cx.send_sync_message(SyncMessage::AddPeersForceRangeSync {
peers: lookup.all_peers().copied().collect(),
head_slot: tip_lookup.peek_downloaded_block_slot(),
head_root: parent_chain_tip,
});
} else {
// Should never happen, log error and continue the lookup drop
error!(self.log, "Unable to transition lookup to range sync";
"error" => "Parent chain tip lookup not found",
"block_root" => ?parent_chain_tip
);
}
// Do not downscore peers here. Because we can't distinguish a valid chain from
// a malicious one we may penalize honest peers for attempting to discover us a
// valid chain. Until blocks_by_range allows to specify a tip, for example with
// https://github.com/ethereum/consensus-specs/pull/3845 we will have poor
// attributability. A peer can send us garbage blocks over blocks_by_root, and
// then correct blocks via blocks_by_range.
self.drop_lookup_and_children(*lookup_id);
} else {
// Should never happen
error!(self.log, "Unable to transition lookup to range sync";
"error" => "Block to drop lookup not found",
"block_root" => ?block_to_drop
);
}
return false;