Skip to content
15 changes: 12 additions & 3 deletions OPERATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,8 @@ Clean smoke:
| `--no-listen` / `--listen=0` | `listen=0` / `no_listen=` | bind a loopback default; **off** = no clearnet P2P socket |
| `--listen-onion` | `listen_onion=` | **off** — loopback P2P + `ADD_ONION` (`{datadir}/onion/p2p.priv`); needs `--tor-control` and `--max-inbound` > 0 |
| `--no-discover` | `no_discover=` | discover **on**; flag off = no home-IP self-announce; P2P/wallet onions still listed |
| `--only-net NET` | `only_net=` | all nets; repeatable `ipv4` / `ipv6` / `onion` / `i2p` (`cjdns` later) |
| `--only-net NET` | `only_net=` | all nets; repeatable `ipv4` / `ipv6` / `onion` / `i2p` / `cjdns` |
| `--cjdns-reachable` | `cjdns_reachable=` | **off** — `fc00::/8` is unroutable until set; `--only-net=cjdns` requires it |
| `--connect ADDR` | `connect=` (repeatable) | seeds; `IP:port`, Tor v3 `.onion:port`, or `{52}.b32.i2p:port` |
| `--proxy HOST:PORT` | `proxy=` | unset — SOCKS5 for all P2P outbound |
| `--onion HOST:PORT` | `onion=` | unset — SOCKS5 for onion destinations |
Expand Down Expand Up @@ -453,7 +454,7 @@ sends `tx`, and disconnects. This is **not** Dandelion++. If that
one-shot fails, the tx stays in the mempool and is still not INV'd;
confirmation can still arrive in a block.
`--onion HOST:PORT` stores a separate SOCKS endpoint for onion destinations.
`--only-net onion` (repeatable with `ipv4`/`ipv6`/`i2p`) filters dial and learn;
`--only-net onion` (repeatable with `ipv4`/`ipv6`/`i2p`/`cjdns`) filters dial and learn;
onion requires `--proxy` or `--onion`. `--connect foo.onion:8333` is a start
error when the v3 checksum is invalid. The peers file is `rbitcoin-peers-v2`
(v1 IPv4/IPv6 still loads).
Expand Down Expand Up @@ -488,10 +489,18 @@ error. Unset: I2P rows may still load from `peers` v2 but are not dialed.
`--only-net i2p` without `--i2p-sam` is a start error. `--i2p-accept-incoming`
creates a persistent local destination (`{datadir}/i2p/p2p.priv`, 0600) and
`STREAM FORWARD`s to the P2P bind. With `--listen=0` that is a start error
naming `--listen` (overlay incoming still needs a loopback P2P accept). NixOS:
unless `--listen-onion` provides a loopback accept. NixOS:
`services.rbitcoin.i2p.sam` / `i2p.acceptIncoming`; the unit `After`/`Wants`
`i2pd.service` when SAM is set. Do not start i2pd from this module.

`--cjdns-reachable` treats BIP155 `fc00::/8` as the kernel CJDNS overlay: dial
with ordinary TCP (OS routing), advertise a `--listen` on that IPv6, keep
tagged rows in `peers` v2. Off (default): do not dial CJDNS; do not treat
`fc00::/8` as advertisable. `--only-net=cjdns` without the flag is a start
error. `--listen [fc00:…]:port` binds that address when the OS has it; no
cjdns daemon in-process and no TUN in CI. NixOS: `cjdns.reachable`;
`After`/`Wants` `cjdns.service`. Do not start a cjdns router from this module.

`--datadir` holds the node root (`store/`, `mempool/`, `peers`, `rpc.token`, `rpc.sock`).
Omit `--datadir-cold` and cold files live there too. Set it to put the large
rarely-read Class A **inwit** stem (`inwit.body` + `inwit.loc`, ~486 GiB + loc
Expand Down
40 changes: 39 additions & 1 deletion crates/rbitcoin-net/src/ephemeral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(crate) fn isolated_broadcast_targets(am: &AddrMan, max: usize) -> Vec<NetAdd
NetAddr::Onion { .. } => onions.push(e.addr),
NetAddr::Ip(_) => ips.push(e.addr),
NetAddr::I2p { .. } => {}
NetAddr::Cjdns { .. } => {}
}
}
let mut out = Vec::new();
Expand Down Expand Up @@ -114,7 +115,12 @@ pub fn spawn_isolated_broadcast_loop(
}
let mut rx = mp.subscribe_isolated();
tokio::spawn(async move {
while let Ok(txid) = rx.recv().await {
loop {
let txid = match rx.recv().await {
Ok(txid) => txid,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
};
let Some(tx) = mp.get_tx(&txid) else {
continue;
};
Expand Down Expand Up @@ -428,6 +434,38 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}

#[tokio::test]
async fn ephemeral_broadcast_loop_survives_lagged_kicks() {
use bitcoin::hashes::Hash;
use bitcoin::Txid;

let (dir, hub) = crate::chain::tiny_regtest_hub_labeled("iso-lag");
let mp = MempoolHub::open(dir.join("mp"), Arc::clone(&hub.query)).unwrap();
mp.set_isolated_broadcast(true);
let am = Arc::new(Mutex::new(AddrMan::new()));
let h = spawn_isolated_broadcast_loop(
mp.clone(),
Dialer::Direct,
am,
Magic::REGTEST,
"/rbitcoin:test/".into(),
);
tokio::time::sleep(Duration::from_millis(20)).await;
for i in 0..40u8 {
mp.mark_local_origin(Txid::from_byte_array([i; 32]));
}
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
!h.is_finished(),
"broadcast Lagged must not stop isolated send"
);
mp.mark_local_origin(dummy_tx().compute_txid());
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(!h.is_finished());
h.abort();
let _ = std::fs::remove_dir_all(dir);
}

#[tokio::test]
async fn ephemeral_broadcast_known_tx_no_addrman_targets() {
let tx = dummy_tx();
Expand Down
4 changes: 4 additions & 0 deletions crates/rbitcoin-net/src/ibd/assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,10 @@ mod tests {
PeerSlot {
id,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 18444 + id as u16),
net: crate::NetAddr::from_socket(SocketAddr::new(
IpAddr::V4(Ipv4Addr::LOCALHOST),
18444 + id as u16,
)),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 100,
Expand Down
12 changes: 8 additions & 4 deletions crates/rbitcoin-net/src/ibd/dial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ pub(crate) fn apply_dial_result(book: &mut AddrMan, result: &DialBatchResult) {
book.note_attempt_addr(addr);
}
for s in &result.slots {
book.note_connected(s.addr);
book.note_connected_addr(s.net);
}
for &(addr, kind) in &result.failed {
book.note_connect_failed_addr(addr, kind == DialFailKind::Incompatible);
Expand Down Expand Up @@ -442,8 +442,7 @@ pub(crate) fn dial_blocked_addrs(
cooldown: &HashMap<SocketAddr, Instant>,
now: Instant,
) -> HashSet<crate::NetAddr> {
let mut blocked: HashSet<crate::NetAddr> =
slots.iter().map(|s| crate::NetAddr::Ip(s.addr)).collect();
let mut blocked: HashSet<crate::NetAddr> = slots.iter().map(|s| s.net).collect();
for (&addr, &until) in cooldown {
if until > now {
blocked.insert(crate::NetAddr::Ip(addr));
Expand All @@ -454,7 +453,11 @@ pub(crate) fn dial_blocked_addrs(

/// Live slot addrs whose netgroups occupy outbound diversity (cooldown is exclude-only).
pub(crate) fn alive_dial_addrs(slots: &[PeerSlot]) -> Vec<SocketAddr> {
slots.iter().filter(|s| s.alive).map(|s| s.addr).collect()
slots
.iter()
.filter(|s| s.alive)
.filter_map(|s| s.net.socket_addr())
.collect()
}

pub(crate) fn expire_addr_cooldown(cooldown: &mut HashMap<SocketAddr, Instant>, now: Instant) {
Expand Down Expand Up @@ -648,6 +651,7 @@ mod tests {
PeerSlot {
id,
addr: a,
net: crate::NetAddr::from_socket(a),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 0,
Expand Down
20 changes: 20 additions & 0 deletions crates/rbitcoin-net/src/ibd/events/confirm_reject_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,7 @@ fn confirmed_height_mids_blocked_while_densify_ahead_leaves_tip_hole() {
let slot = PeerSlot {
id: 0,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 18444),
net: crate::NetAddr::from_socket(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 18444)),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 100,
Expand Down Expand Up @@ -1201,6 +1202,7 @@ fn zombie_pending_mid_at_confirmed_height_never_reget() {
let slot = PeerSlot {
id: 0,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 18445),
net: crate::NetAddr::from_socket(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 18445)),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 100,
Expand Down Expand Up @@ -1800,6 +1802,7 @@ fn apply_peer_event_body_and_control_surface() {
PeerSlot {
id,
addr: a,
net: crate::NetAddr::from_socket(a),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 10,
Expand Down Expand Up @@ -2036,6 +2039,10 @@ fn apply_peer_event_repeat_headers_skips_ensure_header_fk() {
PeerSlot {
id: 1,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1)), 18444),
net: crate::NetAddr::from_socket(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1)),
18444,
)),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 10,
Expand Down Expand Up @@ -2177,6 +2184,10 @@ fn apply_peer_event_block_framed_bq_horizon_and_headers_done() {
PeerSlot {
id,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 18444),
net: crate::NetAddr::from_socket(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
18444,
)),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 5,
Expand Down Expand Up @@ -2456,6 +2467,10 @@ fn block_framed_raw_offers_body_queue_with_confirm_feed() {
PeerSlot {
id,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 18444),
net: crate::NetAddr::from_socket(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
18444,
)),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 5,
Expand Down Expand Up @@ -2593,6 +2608,10 @@ fn known_headers_re_admit_to_ordered_after_tip_drain() {
PeerSlot {
id,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 18444),
net: crate::NetAddr::from_socket(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
18444,
)),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 5,
Expand Down Expand Up @@ -2744,6 +2763,7 @@ fn path_slot_first_wins_chained_via_headers() {
PeerSlot {
id,
addr: a,
net: crate::NetAddr::from_socket(a),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 10,
Expand Down
4 changes: 1 addition & 3 deletions crates/rbitcoin-net/src/ibd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,9 +674,7 @@
let mut n = 0usize;
for s in result.slots {
// Race: same addr may have connected on another path.
if blocked.contains(&crate::NetAddr::Ip(s.addr))
|| st.slots.iter().any(|x| x.addr == s.addr)
{
if blocked.contains(&s.net) || st.slots.iter().any(|x| x.net == s.net) {

Check warning on line 677 in crates/rbitcoin-net/src/ibd/mod.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace == with != in ibd_cancellable

Check warning on line 677 in crates/rbitcoin-net/src/ibd/mod.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace || with && in ibd_cancellable
warn!(
"ibd: drop duplicate/cooldown dial peer[{}] {}",
s.id, s.addr
Expand Down
6 changes: 6 additions & 0 deletions crates/rbitcoin-net/src/ibd/peer_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ impl PeerEventSinks {
pub(crate) struct PeerSlot {
pub id: usize,
pub addr: SocketAddr,
pub net: crate::NetAddr,
pub cmd_tx: mpsc::UnboundedSender<PeerCmd>,
/// Hashes currently requested from this peer.
pub in_flight: HashSet<BlockHash>,
Expand Down Expand Up @@ -426,6 +427,7 @@ pub(crate) async fn spawn_peer(
Ok(PeerSlot {
id,
addr: version_socket,
net: addr,
cmd_tx,
in_flight: HashSet::new(),
peer_height,
Expand Down Expand Up @@ -507,6 +509,10 @@ mod tests {
PeerSlot {
id,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 18444),
net: crate::NetAddr::from_socket(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
18444,
)),
cmd_tx,
in_flight: HashSet::new(),
peer_height: 100,
Expand Down
2 changes: 1 addition & 1 deletion crates/rbitcoin-net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub use net_permissions::{
apply_implicit, parse_whitebind, parse_whitelist, NetPermTable, NetPermissionFlags,
WhitebindGrant, WhitelistGrant, DEFAULT_WHITELISTFORCERELAY, DEFAULT_WHITELISTRELAY,
};
pub use netaddr::{addr_allowed, NetAddr, OnlyNet};
pub use netaddr::{is_cjdns_ip, NetAddr, OnlyNet};
pub use netgroup::netgroup;
pub use peer::{
drain_pending_now, flush_tx_invs, force_announce_txid, local_service_flags, run_feeler_timed,
Expand Down
Loading
Loading