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
3 changes: 3 additions & 0 deletions changelog.d/onion-eviction-cooldown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixed

- Track IBD eviction cooldowns and strikes by peer network identity, so onion peers on the same port no longer share strike accounting and their cooldowns exclude the actual onion targets. Reclaim strike history after an address leaves addrman and its cooldown expires.
83 changes: 42 additions & 41 deletions crates/rbitcoin-ibd/src/ibd/dial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use rbitcoin_net::chain::ChainHub;
use rbitcoin_net::error::NetError;
use rbitcoin_net::peers::{trying_connection_log, PeerConnType};
use rbitcoin_net::seeds::AddrMan;
use rbitcoin_net::NetAddr;
use std::collections::{HashMap, HashSet};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
Expand Down Expand Up @@ -357,29 +358,28 @@ pub fn redial_want(alive: usize, target: usize) -> usize {
///
/// A network failure (EOF, timeout) takes the same strike cooldown as a
/// stall kick. Incompatible peers stay last-resort without that ban.
/// A later successful connect clears the cooldown at the call site.
/// A later successful connect clears the cooldown, but keeps its strike history.
pub fn apply_dial_result(
book: &mut AddrMan,
result: &DialBatchResult,
addr_cooldown: &mut HashMap<SocketAddr, Instant>,
addr_strikes: &mut HashMap<SocketAddr, u8>,
addr_cooldown: &mut HashMap<NetAddr, Instant>,
addr_strikes: &mut HashMap<NetAddr, u8>,
now: Instant,
) {
for &addr in &result.attempted {
book.note_attempt_addr(addr);
}
for s in &result.slots {
book.note_connected_addr(s.net);
addr_cooldown.remove(&s.net);
}
for &(addr, kind) in &result.failed {
let incompatible = kind == DialFailKind::Incompatible;
book.note_connect_failed_addr(addr, incompatible);
if incompatible {
continue;
}
if let Some(sock) = addr.socket_addr() {
record_stall_kick(addr_cooldown, addr_strikes, sock, now);
}
record_stall_kick(addr_cooldown, addr_strikes, addr, now);
}
}

Expand All @@ -389,7 +389,7 @@ pub fn apply_dial_result(
pub fn replacement_available(
book: &AddrMan,
slots: &[PeerSlot],
cooldown: &HashMap<SocketAddr, Instant>,
cooldown: &HashMap<NetAddr, Instant>,
now: Instant,
) -> bool {
let live: HashSet<rbitcoin_net::NetAddr> =
Expand All @@ -398,9 +398,7 @@ pub fn replacement_available(
if !book.is_dialable(addr) || live.contains(&addr) {
return false;
}
let cooling = addr
.socket_addr()
.is_some_and(|sock| cooldown.get(&sock).is_some_and(|until| *until > now));
let cooling = cooldown.get(&addr).is_some_and(|until| *until > now);
!cooling && !book.connect_failed(addr)
})
}
Expand Down Expand Up @@ -497,7 +495,7 @@ pub fn admit_cooldown_fallback(
book: &AddrMan,
exclude: &mut HashSet<rbitcoin_net::NetAddr>,
occupied: &[SocketAddr],
cooldown: &HashMap<SocketAddr, Instant>,
cooldown: &HashMap<NetAddr, Instant>,
now: Instant,
live: &HashSet<rbitcoin_net::NetAddr>,
) -> Option<rbitcoin_net::NetAddr> {
Expand All @@ -512,10 +510,7 @@ pub fn admit_cooldown_fallback(
.iter()
.copied()
.filter(|a| book.is_dialable(*a) && !live.contains(a))
.filter(|a| {
a.socket_addr()
.is_some_and(|sock| cooldown.get(&sock).is_some_and(|until| *until > now))
})
.filter(|a| cooldown.get(a).is_some_and(|until| *until > now))
.collect();
let pick = cooling
.iter()
Expand All @@ -536,13 +531,13 @@ pub fn admit_cooldown_fallback(
/// Addrs we must not dial: currently connected/slot-held + still-cooling stall bans.
pub fn dial_blocked_addrs(
slots: &[PeerSlot],
cooldown: &HashMap<SocketAddr, Instant>,
cooldown: &HashMap<NetAddr, Instant>,
now: Instant,
) -> HashSet<rbitcoin_net::NetAddr> {
let mut blocked: HashSet<rbitcoin_net::NetAddr> = slots.iter().map(|s| s.net).collect();
for (&addr, &until) in cooldown {
if until > now {
blocked.insert(rbitcoin_net::NetAddr::Ip(addr));
blocked.insert(addr);
}
}
blocked
Expand All @@ -557,8 +552,14 @@ pub fn alive_dial_addrs(slots: &[PeerSlot]) -> Vec<SocketAddr> {
.collect()
}

pub fn expire_addr_cooldown(cooldown: &mut HashMap<SocketAddr, Instant>, now: Instant) {
pub fn expire_addr_cooldown(
cooldown: &mut HashMap<NetAddr, Instant>,
strikes: &mut HashMap<NetAddr, u8>,
book: &AddrMan,
now: Instant,
) {
cooldown.retain(|_, until| *until > now);
strikes.retain(|addr, _| cooldown.contains_key(addr) || book.entry_of(addr).is_some());
}

/// Cooldown after the Nth stall/relative-slow kick of this addr this process.
Expand All @@ -579,11 +580,11 @@ pub fn disconnect_peer(st: &mut super::state::IbdWorkState, peer: usize) {
};
let net = st.slots[idx].net;
let _ = st.slots[idx].cmd_tx.send(PeerCmd::Shutdown);
if let Some(sock) = net.socket_addr().filter(|s| !s.ip().is_unspecified()) {
if !net.socket_addr().is_some_and(|s| s.ip().is_unspecified()) {
record_stall_kick(
&mut st.addr_cooldown,
&mut st.addr_strikes,
sock,
net,
Instant::now(),
);
}
Expand All @@ -593,9 +594,9 @@ pub fn disconnect_peer(st: &mut super::state::IbdWorkState, peer: usize) {

/// Bump the process-local strike count and set `addr_cooldown`.
pub fn record_stall_kick(
addr_cooldown: &mut HashMap<SocketAddr, Instant>,
strikes: &mut HashMap<SocketAddr, u8>,
addr: SocketAddr,
addr_cooldown: &mut HashMap<NetAddr, Instant>,
strikes: &mut HashMap<NetAddr, u8>,
addr: NetAddr,
now: Instant,
) -> Duration {
let n = {
Expand All @@ -611,7 +612,7 @@ pub fn record_stall_kick(
/// Handshake with no block bytes: last-resort + stall cooldown (not a stall disconnect).
pub fn note_dead_without_block_bytes(
book: &mut AddrMan,
addr_cooldown: &mut HashMap<SocketAddr, Instant>,
addr_cooldown: &mut HashMap<NetAddr, Instant>,
addr: rbitcoin_net::NetAddr,
first_data_ms: u64,
now: Instant,
Expand All @@ -620,21 +621,21 @@ pub fn note_dead_without_block_bytes(
return;
}
book.note_connect_failed_addr(addr, false);
if let Some(sock) = addr.socket_addr().filter(|s| !s.ip().is_unspecified()) {
addr_cooldown.insert(sock, now + STALL_ADDR_COOLDOWN);
if !addr.socket_addr().is_some_and(|s| s.ip().is_unspecified()) {
addr_cooldown.insert(addr, now + STALL_ADDR_COOLDOWN);
}
}

/// Misbehavior-threshold death. Cools the dial even after a block body was counted.
pub fn note_misbehavior_dead(
book: &mut AddrMan,
addr_cooldown: &mut HashMap<SocketAddr, Instant>,
addr_cooldown: &mut HashMap<NetAddr, Instant>,
addr: rbitcoin_net::NetAddr,
now: Instant,
) {
book.note_connect_failed_addr(addr, false);
if let Some(sock) = addr.socket_addr().filter(|s| !s.ip().is_unspecified()) {
addr_cooldown.insert(sock, now + STALL_ADDR_COOLDOWN);
if !addr.socket_addr().is_some_and(|s| s.ip().is_unspecified()) {
addr_cooldown.insert(addr, now + STALL_ADDR_COOLDOWN);
}
}

Expand Down Expand Up @@ -668,8 +669,8 @@ pub fn disconnect_stalled_block_peers(
slots: &mut [PeerSlot],
inflight: &mut HashMap<bitcoin::BlockHash, super::state::InflightReq>,
body: &mut super::body::BodyPresence,
addr_cooldown: &mut HashMap<SocketAddr, Instant>,
addr_strikes: &mut HashMap<SocketAddr, u8>,
addr_cooldown: &mut HashMap<NetAddr, Instant>,
addr_strikes: &mut HashMap<NetAddr, u8>,
now: Instant,
stall: Duration,
) -> Vec<bitcoin::BlockHash> {
Expand All @@ -691,17 +692,17 @@ pub fn disconnect_stalled_block_peers_at(
slots: &mut [PeerSlot],
inflight: &mut HashMap<bitcoin::BlockHash, super::state::InflightReq>,
body: &mut super::body::BodyPresence,
addr_cooldown: &mut HashMap<SocketAddr, Instant>,
addr_strikes: &mut HashMap<SocketAddr, u8>,
addr_cooldown: &mut HashMap<NetAddr, Instant>,
addr_strikes: &mut HashMap<NetAddr, u8>,
now: Instant,
stall: Duration,
now_ms: u64,
) -> Vec<bitcoin::BlockHash> {
let stall = stall.max(Duration::from_secs(30));
let stall_ms = stall.as_millis() as u64;
let mut on_wire = 0usize;
let mut quiet: Vec<(usize, usize, SocketAddr, u64)> = Vec::new();
let mut withholding: Vec<(usize, usize, SocketAddr, u64)> = Vec::new();
let mut quiet: Vec<(usize, usize, NetAddr, u64)> = Vec::new();
let mut withholding: Vec<(usize, usize, NetAddr, u64)> = Vec::new();
for s in slots.iter() {
if !s.alive || s.in_flight.is_empty() {
continue;
Expand All @@ -713,7 +714,7 @@ pub fn disconnect_stalled_block_peers_at(
let row = (
s.id,
s.in_flight.len(),
s.addr,
s.net,
s.bytes_rx_total.load(Ordering::Relaxed),
);
if super::peer_io::getdata_on_wire_quiet(s, now_ms, stall_ms) {
Expand Down Expand Up @@ -770,8 +771,8 @@ pub fn disconnect_relative_slow_block_peers(
slots: &mut [PeerSlot],
inflight: &mut HashMap<bitcoin::BlockHash, super::state::InflightReq>,
body: &mut super::body::BodyPresence,
addr_cooldown: &mut HashMap<SocketAddr, Instant>,
addr_strikes: &mut HashMap<SocketAddr, u8>,
addr_cooldown: &mut HashMap<NetAddr, Instant>,
addr_strikes: &mut HashMap<NetAddr, u8>,
now: Instant,
book: &AddrMan,
suspect: &mut Option<(usize, u64)>,
Expand All @@ -796,8 +797,8 @@ pub fn disconnect_relative_slow_block_peers_at(
slots: &mut [PeerSlot],
inflight: &mut HashMap<bitcoin::BlockHash, super::state::InflightReq>,
body: &mut super::body::BodyPresence,
addr_cooldown: &mut HashMap<SocketAddr, Instant>,
addr_strikes: &mut HashMap<SocketAddr, u8>,
addr_cooldown: &mut HashMap<NetAddr, Instant>,
addr_strikes: &mut HashMap<NetAddr, u8>,
now: Instant,
book: &AddrMan,
suspect: &mut Option<(usize, u64)>,
Expand Down Expand Up @@ -829,7 +830,7 @@ pub fn disconnect_relative_slow_block_peers_at(
*suspect = None;
return Vec::new();
};
let addr = slot.addr;
let addr = slot.net;
let n_work = slot.in_flight.len();
let bps = samples
.iter()
Expand Down
5 changes: 1 addition & 4 deletions crates/rbitcoin-ibd/src/ibd/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,10 +1091,7 @@ pub fn apply_peer_dead(
);
}
let lat = s.first_data_ms.saturating_sub(s.connected_ms);
let cooled = s
.net
.socket_addr()
.is_some_and(|sock| st.addr_cooldown.contains_key(&sock));
let cooled = st.addr_cooldown.contains_key(&s.net);
peer_book.apply_ibd_dead_speed_addr(s.net, lat, s.rate.bps(), cooled);
}
let freed = release_peer_block_work(&mut st.slots, &mut st.inflight, &mut st.body, peer);
Expand Down
14 changes: 6 additions & 8 deletions crates/rbitcoin-ibd/src/ibd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,12 @@ pub async fn ibd_cancellable(
&mut st.relative_slow_last_kick_ms,
));
st.reopen_for_densify(&freed);
expire_addr_cooldown(&mut st.addr_cooldown, now);
expire_addr_cooldown(
&mut st.addr_cooldown,
&mut st.addr_strikes,
peer_sess.book(),
now,
);
cadence.mark_peer_slow(now);
}
st.slots.retain(|s| s.alive);
Expand All @@ -970,13 +975,6 @@ pub async fn ibd_cancellable(
&mut st.addr_strikes,
Instant::now(),
);
// A successful dial ends that address's cooldown, including
// the one we admitted while every candidate was cooling.
for s in &result.slots {
if let Some(sock) = s.net.socket_addr() {
st.addr_cooldown.remove(&sock);
}
}
let blocked = dial_blocked_addrs(&st.slots, &st.addr_cooldown, Instant::now());
let mut n = 0usize;
for s in result.slots {
Expand Down
8 changes: 4 additions & 4 deletions crates/rbitcoin-ibd/src/ibd/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ use super::assign_plan::compact_ordered;
use super::body::{BodyPresence, BodyPresenceSizes};
use super::reorg::IbdReorgState;
use bitcoin::BlockHash;
use rbitcoin_net::NetAddr;
use rbitcoin_primitives::Fk;
use std::collections::{HashMap, HashSet, VecDeque};
use std::net::SocketAddr;
use std::time::Instant;

use super::peer_io::PeerSlot;
Expand Down Expand Up @@ -174,12 +174,12 @@ pub struct IbdWorkState {
/// Rotates which peer is offered work first.
pub assign_rot: usize,
/// Stall-disconnect cooldowns (addr → until).
pub addr_cooldown: HashMap<SocketAddr, Instant>,
pub addr_cooldown: HashMap<NetAddr, Instant>,
/// Per-peer addr token buckets (peer id → bucket), as on tip-follow
/// sessions. Dropped when the peer dies.
pub addr_buckets: HashMap<usize, rbitcoin_net::peer::AddrTokenBucket>,
/// Process-local stall/relative-slow kick counts (not persisted).
pub addr_strikes: HashMap<SocketAddr, u8>,
/// Process-local kick counts for addresses in addrman or still cooling.
pub addr_strikes: HashMap<NetAddr, u8>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: addr_strikes is never removed. Cooldown expiry does not drop it, and dial failures now insert one entry per onion or I2P. Those keys outlive addrman eviction for the rest of the process.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c09af01. Cooldown expiry now also removes strikes when an identity is absent from addrman and has no active cooldown. Retained addresses keep their strike history across expiry and successful redials. Added a regression covering onion/I2P eviction and retained IPv4/IPv6 escalation, plus the missing changelog fragment.

/// Relative-slow hysteresis: peer id + first Gate B fail ms.
pub relative_slow_suspect: Option<(usize, u64)>,
/// Mono ms of last relative-slow disconnect (`0` = never).
Expand Down
Loading
Loading