Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ before 1.0).

### Changed

- **IBD exits to tip follow at the peer horizon:** leftover off-path
`getdata` is dropped so catch-up can complete; if peers then advertise
a higher tip (`lag > 2`), `headers_done` unlatches and `getheaders`
resumes. Near-tip (`lag ≤ 2`) still does not re-fan.

- **In-flight keep-until:** pin layers stay until drain+fence **and**
`class_a_hi >= until` (`until = lookup_started_hi.max(hi)` frozen at
write). Stamp walks `InFlightView` only (then live_union, then TipOnly).
Expand Down
68 changes: 68 additions & 0 deletions crates/rbitcoin-net/src/ibd/assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ pub(crate) fn prune_satisfied_inflight(
}
}

/// Drop getdata that cannot feed the work path or a reorg gather.
///
/// Off-path leftovers otherwise block [`path_drained`](super::exit::path_drained)
/// forever (mainnet 08:16:23: ordered empty, h2h=0, inflight=7).
pub(crate) fn prune_off_path_inflight(st: &mut IbdWorkState) {
let reorg_need: HashSet<BlockHash> = st.reorg.need_getdata().into_iter().collect();
let drop: Vec<BlockHash> = st
.inflight
.keys()
.copied()
.filter(|h| {
if st.ordered_set.contains(h) || reorg_need.contains(h) {
return false;
}
if let Some(&ht) = st.hash_height.get(h) {
if st.is_on_path(h, ht) {
return false;
}
}
true
})
.collect();
for h in drop {
clear_hash_inflight(&mut st.slots, &mut st.inflight, h);
}
}

/// Record `peer` as requesting `hash` (tip-hole / park race may accumulate peers).
pub(crate) fn inflight_add_peer(
inflight: &mut HashMap<BlockHash, state::InflightReq>,
Expand Down Expand Up @@ -107,6 +134,7 @@ pub(crate) fn assign_work_ordered(
}

prune_satisfied_inflight(&mut st.slots, &mut st.inflight, hub);
prune_off_path_inflight(st);

let _ = super::reorg::consider_disconnected_heavier(st, hub);

Expand Down Expand Up @@ -805,6 +833,46 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}

/// Off-path getdata (mainnet 08:16:23: ordered empty, h2h=0, inflight=7)
/// must not occupy slots; tip+1 and reorg-need hashes stay.
#[test]
fn prune_off_path_inflight_drops_orphans_keeps_path_and_reorg() {
let (dir, hub) = tmp_hub();
hub.ensure_genesis().unwrap();
let mut st = IbdWorkState::new(vec![dummy_slot(0)], hub.tip_hash(), hub.tip_height());
assert!(st.ordered.is_empty());
assert!(st.height_to_hash.is_empty() || st.height_to_hash.len() <= 1);

for i in 0..7u32 {
let hash = h(1000 + i);
st.slots[0].in_flight.insert(hash);
inflight_add_peer(&mut st.inflight, hash, 0);
}
let want = h(0x11);
let ht = hub.tip_height().unwrap_or(0).saturating_add(1);
st.record_height(want, ht);
st.slots[0].in_flight.insert(want);
inflight_add_peer(&mut st.inflight, want, 0);
let reorg_h = h(0x22);
st.reorg.register_explore(std::iter::once(reorg_h), None);
st.slots[0].in_flight.insert(reorg_h);
inflight_add_peer(&mut st.inflight, reorg_h, 0);
assert_eq!(st.inflight.len(), 9);

prune_off_path_inflight(&mut st);

assert_eq!(st.inflight.len(), 2, "orphans dropped; path+reorg kept");
assert!(st.inflight.contains_key(&want), "tip+1 occupant stays");
assert!(st.inflight.contains_key(&reorg_h), "reorg need stays");
for i in 0..7u32 {
let hash = h(1000 + i);
assert!(!st.inflight.contains_key(&hash), "orphan {i} dropped");
assert!(!st.slots[0].in_flight.contains(&hash));
}

let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn scale_and_saturated_helpers() {
assert!(!archive_pipeline_saturated(0, 20, false));
Expand Down
73 changes: 73 additions & 0 deletions crates/rbitcoin-net/src/ibd/exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ pub(crate) fn empty_path_header_fan(lag: u32, inflight: usize, alive: usize) ->
alive.min(4).max(1)
}

/// Clear `headers_done` so empty-path `getheaders` can resume.
///
/// Latch is only for lag ≤ 2 (stop the tip storm). When peers advertise a
/// higher tip, unlatch even if leftover getdata is still inflight — that
/// used to sit behind [`path_drained`] and stall until SIGINT.
#[inline]
pub(crate) fn should_unlatch_headers_done(st: &IbdWorkState, tip_h: u32) -> bool {
st.headers_done && st.ordered.is_empty() && header_lag_behind_peers(st, tip_h) > 2
}

/// Full `seed_work_path_from_store` (O(header_count) walk) while empty-lagging.
///
/// Must stay rare: mainnet ~1M headers ≈ 200–300ms per call. Same cadence as
Expand All @@ -74,6 +84,10 @@ pub(crate) fn should_reseed_work_path_on_empty_lag(streak: u32) -> bool {
}

/// Work path idle: no ordered hashes, no inflight getdata.
///
/// Off-path leftover getdata is dropped by
/// [`super::assign::prune_off_path_inflight`] before this is consulted — not a
/// second meaning of drained.
#[inline]
pub fn path_drained(st: &IbdWorkState) -> bool {
st.ordered.is_empty() && st.inflight.is_empty()
Expand Down Expand Up @@ -210,6 +224,38 @@ mod tests {
near.max_ready_height = 105;
assert!(!peer_caught_up(&near, 100));
assert_eq!(header_lag_behind_peers(&near, 100), 0); // archived ≥ peer

// Mainnet 08:16:23: ordered empty, headers_done, tip=horizon, 7 off-path
// inflight. Prune then drain — do not treat leftover getdata as work.
use super::super::assign::prune_off_path_inflight;
use super::super::state::InflightReq;
let mut at_horizon = IbdWorkState::new(Vec::new(), None, Some(964_108));
at_horizon.max_peer_height = 964_108;
at_horizon.max_ready_height = 964_108;
at_horizon.headers_done = true;
for i in 1u8..=7 {
at_horizon
.inflight
.insert(BlockHash::from_byte_array([i; 32]), InflightReq::new(0));
}
assert!(!path_drained(&at_horizon));
assert!(!catchup_complete_after_drain(&at_horizon, 964_108));
prune_off_path_inflight(&mut at_horizon);
assert!(path_drained(&at_horizon));
assert!(catchup_complete_after_drain(&at_horizon, 964_108));

// Mid-chain on-path inflight (tip+1 in h2h) must not complete after prune.
let mut mid_inf = IbdWorkState::new(Vec::new(), None, Some(161_249));
mid_inf.max_peer_height = 958_820;
mid_inf.max_ready_height = 161_000;
mid_inf.headers_done = true;
let on_path = BlockHash::from_byte_array([0x2a; 32]);
mid_inf.record_height(on_path, 161_250);
mid_inf.inflight.insert(on_path, InflightReq::new(0));
prune_off_path_inflight(&mut mid_inf);
assert!(mid_inf.inflight.contains_key(&on_path));
assert!(!path_drained(&mid_inf));
assert!(!catchup_complete_after_drain(&mid_inf, 161_249));
}

/// Empty-headers lag WARN/reget cadence (mainnet log flood regression).
Expand Down Expand Up @@ -269,4 +315,31 @@ mod tests {
assert_eq!(empty_path_header_fan(100, 5, 29), 1);
assert_eq!(empty_path_header_fan(100, 0, 2), 2);
}

/// Unlatch `headers_done` on lag>2 even with leftover inflight; never at lag≤2.
#[test]
fn should_unlatch_headers_done() {
use super::super::state::InflightReq;
use bitcoin::hashes::Hash;
use bitcoin::BlockHash;

let mut st = IbdWorkState::new(Vec::new(), None, Some(100));
st.max_peer_height = 105;
st.max_ready_height = 100;
st.headers_done = true;
for i in 1u8..=7 {
st.inflight
.insert(BlockHash::from_byte_array([i; 32]), InflightReq::new(0));
}
assert_eq!(header_lag_behind_peers(&st, 100), 5);
assert!(super::should_unlatch_headers_done(&st, 100));

st.max_peer_height = 100;
assert_eq!(header_lag_behind_peers(&st, 100), 0);
assert!(!super::should_unlatch_headers_done(&st, 100));

st.max_peer_height = 105;
st.ordered.push_back(BlockHash::from_byte_array([0xab; 32]));
assert!(!super::should_unlatch_headers_done(&st, 100));
}
}
11 changes: 5 additions & 6 deletions crates/rbitcoin-net/src/ibd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ use events::{
};
use exit::{
all_peers_dead_action, catchup_complete_after_drain, empty_path_header_fan,
header_lag_behind_peers, path_drained, peer_caught_up, AllPeersDead,
header_lag_behind_peers, path_drained, peer_caught_up, should_unlatch_headers_done,
AllPeersDead,
};
use path::{seed_work_path_from_store, work_path_tips};
use peer_io::{PeerCmd, PeerEvent, PeerEventSinks};
Expand Down Expand Up @@ -595,6 +596,9 @@ pub async fn ibd_cancellable(
);
let under_hard = live < MAX_ORDERED_HEADERS;
let under_soft = live < ORDERED_HEADERS_SOFT_CAP;
if should_unlatch_headers_done(&st, hub.tip_height().unwrap_or(0)) {
st.headers_done = false;
}
if !st.headers_done && under_hard && (under_soft || need_ready_headroom) {
let tip_h = hub.tip_height().unwrap_or(0);
let lag = header_lag_behind_peers(&st, tip_h);
Expand Down Expand Up @@ -945,11 +949,6 @@ pub async fn ibd_cancellable(
);
break;
}
if header_lag_behind_peers(&st, tip_h) > 2 {
st.headers_done = false;
let tips = work_path_tips(&st);
let _ = request_headers(&st.slots, &hub, &mut st.header_req_seq, &tips);
}
}
// All peers dead — never treat mid-chain peer death as catch-up complete.
if st.slots.iter().all(|s| !s.alive) {
Expand Down
Loading