From f2af01882a98454137e96746b716e9b850f86cf2 Mon Sep 17 00:00:00 2001 From: rbitcoin-grok Date: Sat, 29 Aug 2026 13:29:26 -0700 Subject: [PATCH] =?UTF-8?q?ibd:=20clamp=20assign-stop=20densify=20to=20con?= =?UTF-8?q?firm=20window=20=E2=88=A9=20fetched?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assign-stop was filling holes out to fetched_hi, which ballooned the body queue when confirm stalled far behind a high fetched horizon. At/over stop, band_hi is now min(confirm_window_hi, fetched_hi, densify_hi) so densify stays in the ~1min tip-rate window and does not grow past fetched. Co-authored-by: Cursor --- OPERATOR.md | 2 +- crates/rbitcoin-net/src/ibd/assign.rs | 26 +++++++++------- crates/rbitcoin-query/src/soft_densify.rs | 38 +++++++++++++++++------ crates/rbitcoin-store/src/block_queue.rs | 8 ++--- docs/ibd-memory.md | 4 +-- 5 files changed, 50 insertions(+), 28 deletions(-) diff --git a/OPERATOR.md b/OPERATOR.md index 7e4c18c6..9283e56e 100644 --- a/OPERATOR.md +++ b/OPERATOR.md @@ -212,7 +212,7 @@ Requires **tip mode** (`node: catch-up complete … tip tracking`). During IBD u | Line | Level | Use | |------|-------|-----| -| `ibd: progress` | INFO | Tip rate, `loadq`/`scriptq`/`writeq`, `txs=` (Class A / `tx.idx` count), horizon, tip ETA, **`bq soft=n/win RAM=`** (in-RAM body queue; soft densify: under ~100 MiB free ahead, over that only ~1 min confirm window, at/over 1 GiB assign-stop fill holes in the fetched range only) | +| `ibd: progress` | INFO | Tip rate, `loadq`/`scriptq`/`writeq`, `txs=` (Class A / `tx.idx` count), horizon, tip ETA, **`bq soft=n/win RAM=`** (in-RAM body queue; soft densify: under ~100 MiB free ahead, over that only ~1 min confirm window, at/over 1 GiB assign-stop holes within that window and not past fetched_hi) | | `ibd: perf` | DEBUG | Inflight + **`bq soft= RAM=`**; **`load=`** is pin+assemble only. **`load_thr pack/stamp/pin/asm/prune`** is the load OS thread. **`stamp=`** nests **`pack=`** (plan HashMap) vs **`head=`** (leftover TipOnly; IBD skeleton keeps this ~0). **`script=`** is verify ns (`jobs=` / `skip=`); recv/send are wait. **`pin_txid=`** is skeleton hits vs leftover `tx.head` | | `ibd: sizes` | DEBUG | RSS + work path + **`bq soft=` / `RAM=`** + **conf_plans** + confirm pipe | | `ibd: perf_dbg` | DEBUG | µs/blk load/write, pin/edge detail, **plan_batch** (`us/pin_txid` vs `probe/idx/body us/key`) + **class_a commit** | diff --git a/crates/rbitcoin-net/src/ibd/assign.rs b/crates/rbitcoin-net/src/ibd/assign.rs index 729a62db..a2874bb2 100644 --- a/crates/rbitcoin-net/src/ibd/assign.rs +++ b/crates/rbitcoin-net/src/ibd/assign.rs @@ -10,8 +10,9 @@ //! - BQ payload **≤ ~100 MiB** → usual densify ahead to the height horizon //! - BQ payload **> ~100 MiB** → only heights confirm will consume in the //! next **~1 min** at current tip rate ([`rbitcoin_query::soft_densify_band_hi`]) -//! - BQ payload **≥ assign-stop** (default 1 GiB) → holes through the -//! already-fetched height horizon only (BQ max / lookup_taken); do not grow +//! - BQ payload **≥ assign-stop** (default 1 GiB) → holes only within the +//! ~1 min tip-rate window **and** not past fetched_hi (do not grow past +//! fetched; do not densify far holes outside the window) //! - Never request beyond densify horizon; events refuse far bodies too. //! - One body-queue copy per height (receive path drops duplicates). @@ -1661,9 +1662,9 @@ mod tests { /// Serialize env mutators — parallel suite races `bq_assign_stop_bytes`. static BQ_ASSIGN_STOP_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - /// Over assign-stop: fill holes in the already-fetched range, do not grow past it. + /// Over assign-stop: densify within confirm window ∩ fetched; not past window. #[test] - fn densify_over_assign_stop_fills_fetched_range_only() { + fn densify_over_assign_stop_clamps_window_and_fetched() { let _g = BQ_ASSIGN_STOP_ENV_LOCK .lock() .unwrap_or_else(|e| e.into_inner()); @@ -1694,7 +1695,7 @@ mod tests { cfg.window = 128; cfg.per_peer = 64; - for ht in 1u32..=200 { + for ht in 1u32..=500 { let hash = h(ht); st.record_height(hash, ht); st.height_to_hash.insert(ht, hash); @@ -1710,13 +1711,14 @@ mod tests { .unwrap(); st.body.mark_pending(h(ht)); } + // Far fetched_hi=500 trips assign-stop; rate 5 → confirm window 300. let chunk = vec![0u8; 4096]; hub.query - .block_queue_enqueue(80, h(80).to_byte_array(), 80, &chunk) + .block_queue_enqueue(500, h(500).to_byte_array(), 500, &chunk) .unwrap(); - st.body.mark_pending(h(80)); + st.body.mark_pending(h(500)); assert!(hub.query.block_queue_stats().1 >= 2048); - assert_eq!(hub.query.block_queue_max_height(), Some(80)); + assert_eq!(hub.query.block_queue_max_height(), Some(500)); assign_work_ordered(&mut st, &hub, &cfg, &stats, 1, AssignDepth::Full, Some(5.0)); @@ -1728,12 +1730,12 @@ mod tests { assert!( issued_hts .iter() - .any(|&ht| ht > TIP_HOLE_MAX as u32 && ht < 80), - "assign-stop must still densify holes inside fetched range; issued={issued_hts:?}" + .any(|&ht| ht > TIP_HOLE_MAX as u32 && ht <= 300), + "assign-stop must densify holes inside confirm window; issued={issued_hts:?}" ); assert!( - issued_hts.iter().all(|&ht| ht <= 80), - "assign-stop must not grow past fetched horizon 80; issued={issued_hts:?}" + issued_hts.iter().all(|&ht| ht <= 300), + "assign-stop must not issue past window 300 (fetched_hi=500); issued={issued_hts:?}" ); let _ = std::fs::remove_dir_all(dir); diff --git a/crates/rbitcoin-query/src/soft_densify.rs b/crates/rbitcoin-query/src/soft_densify.rs index e21b5809..40bded9d 100644 --- a/crates/rbitcoin-query/src/soft_densify.rs +++ b/crates/rbitcoin-query/src/soft_densify.rs @@ -12,8 +12,9 @@ pub const BQ_SOFT_FREE_BYTES: u64 = 100 * 1024 * 1024; /// rate. Tunable constant — no hysteresis band. pub const BQ_SOFT_CONFIRM_SECS: f64 = 60.0; -/// Default densify assign-stop (1 GiB). Over this, fill holes in the already -/// fetched height range; do not grow past that horizon. Override with +/// Default densify assign-stop (1 GiB). At/over this, densify only holes within +/// the ~1 min tip-rate confirm window **and** not past `fetched_hi` (do not grow +/// past fetched; do not densify far holes outside the window). Override with /// `RBITCOIN_BLOCK_QUEUE_BYTES` / `_GB` (`0` = unlimited). pub const BQ_ASSIGN_STOP_BYTES: u64 = 1024 * 1024 * 1024; @@ -61,8 +62,10 @@ pub fn bq_assign_stop_bytes() -> u64 { /// Inclusive densify band high height for getdata assign. /// -/// - **At/over** `assign_stop_bytes`: holes through `fetched_hi` only (do not -/// grow past the already-fetched horizon). No fetched range → empty band. +/// - **At/over** `assign_stop_bytes`: holes only within the ~1 min tip-rate +/// confirm window **and** not past `fetched_hi` (do not grow past fetched; +/// do not densify far holes outside the window). No usable fetched range → +/// empty band. /// - **Under** [`BQ_SOFT_FREE_BYTES`]: full `densify_hi` (usual densify ahead). /// - **Over** free bytes (and under assign-stop): only heights confirm will /// pick up within [`BQ_SOFT_CONFIRM_SECS`] at current rate — @@ -82,8 +85,14 @@ pub fn soft_densify_band_hi( return densify_hi; } if soft_assign_stopped(depth_bytes, assign_stop_bytes) { + let n = soft_confirm_window_n(rate_blocks_per_s); + let window_hi = if n == 0 { + path_lo.min(densify_hi) + } else { + path_lo.saturating_add(n.saturating_sub(1)).min(densify_hi) + }; return match fetched_hi { - Some(h) if h >= path_lo => h.min(densify_hi), + Some(h) if h >= path_lo => window_hi.min(h), _ => path_lo.saturating_sub(1), }; } @@ -168,7 +177,7 @@ mod tests { } #[test] - fn assign_stop_clamps_to_fetched_horizon() { + fn assign_stop_clamps_to_confirm_window_and_fetched() { let stop = 2048u64; let over = 4096u64; let free = BQ_SOFT_FREE_BYTES; @@ -182,15 +191,26 @@ mod tests { 6, "over free / under 1 GiB still uses confirm window" ); - // Over stop: fill through fetched_hi, do not grow, ignore 1-min window. + // Over stop: min(confirm_window_hi, fetched_hi, densify_hi); rate 5 → window 300. assert_eq!( soft_densify_band_hi(1, 1000, over, Some(5.0), stop, Some(80)), - 80 + 80, + "fetched_hi below window → fetched_hi" + ); + assert_eq!( + soft_densify_band_hi(1, 1000, over, Some(5.0), stop, Some(500)), + 300, + "fetched_hi above window → confirm window, not fetched_hi" ); assert_eq!( soft_densify_band_hi(1, 50, over, Some(5.0), stop, Some(80)), 50, - "fetched_hi clamped to densify_hi" + "densify_hi below fetched and window → densify_hi" + ); + assert_eq!( + soft_densify_band_hi(1, 1000, over, None, stop, Some(80)), + 1, + "rate cold → path_lo only, not full fetched" ); assert_eq!( soft_densify_band_hi(1, 1000, over, Some(5.0), stop, None), diff --git a/crates/rbitcoin-store/src/block_queue.rs b/crates/rbitcoin-store/src/block_queue.rs index c608e434..d7f6655c 100644 --- a/crates/rbitcoin-store/src/block_queue.rs +++ b/crates/rbitcoin-store/src/block_queue.rs @@ -12,10 +12,10 @@ //! //! **Primary capacity** is soft densify assign in the net layer (no hysteresis): //! under ~100 MiB free densify ahead; over ~100 MiB only the next ~1 min of -//! confirm work at tip rate; over the 1 GiB assign-stop (`RBITCOIN_BLOCK_QUEUE_GB` -//! / `_BYTES`) densify fills holes inside the already-fetched height range -//! and does not grow past that horizon. This type **always** accepts -//! already-requested payloads (OOM aside). +//! confirm work at tip rate; at/over the 1 GiB assign-stop (`RBITCOIN_BLOCK_QUEUE_GB` +//! / `_BYTES`) densify holes only within that ~1 min window **and** not past +//! fetched_hi (do not grow past fetched; do not densify far holes outside the +//! window). This type **always** accepts already-requested payloads (OOM aside). use crate::error::StoreError; use std::collections::{BTreeMap, HashMap, HashSet}; diff --git a/docs/ibd-memory.md b/docs/ibd-memory.md index 088307d2..024f3c92 100644 --- a/docs/ibd-memory.md +++ b/docs/ibd-memory.md @@ -8,7 +8,7 @@ IBD path. It is **not** about kernel page cache under FdOnly store files | Structure | Cap / bound | Production clear / evict | |-----------|-------------|---------------------------| -| **In-RAM body queue** | Soft densify assign (no hysteresis): under ~100 MiB free densify ahead; over ~100 MiB only heights confirm will consume in the next ~1 min at tip rate; at/over 1 GiB assign-stop (`RBITCOIN_BLOCK_QUEUE_GB` / `_BYTES`, `0` = unlimited) fill holes through the already-fetched height horizon only (BQ max / lookup_taken) — do not grow past it. **Never** refuse enqueue. `bytes()` is **raw only** | Peer **BlockFramed** enqueues **raw** frame payload and stamps Σ `tx.input` via a CompactSize walk (no `Block` decode). Lookup packs/holds on that count; **dequeues** after load-batch send. Decoded `Arc` + `TxPrecompute` live on **loadq** (cap 14), then scriptq/writeq. **Have-body** (hole / densify / receive) is confirmed ∨ BQ hash ∨ `H ≤ lookup_taken_hi`. **Never both** raw and decoded. **RAM-only by design**. Restart empties BQ+loadq. Logs: `bq soft=n/win RAM=` `loadq=n/14`. | +| **In-RAM body queue** | Soft densify assign (no hysteresis): under ~100 MiB free densify ahead; over ~100 MiB only heights confirm will consume in the next ~1 min at tip rate; at/over 1 GiB assign-stop (`RBITCOIN_BLOCK_QUEUE_GB` / `_BYTES`, `0` = unlimited) holes only within that ~1 min window **and** not past fetched_hi (do not grow past fetched; do not densify far holes outside the window). **Never** refuse enqueue. `bytes()` is **raw only** | Peer **BlockFramed** enqueues **raw** frame payload and stamps Σ `tx.input` via a CompactSize walk (no `Block` decode). Lookup packs/holds on that count; **dequeues** after load-batch send. Decoded `Arc` + `TxPrecompute` live on **loadq** (cap 14), then scriptq/writeq. **Have-body** (hole / densify / receive) is confirmed ∨ BQ hash ∨ `H ≤ lookup_taken_hi`. **Never both** raw and decoded. **RAM-only by design**. Restart empties BQ+loadq. Logs: `bq soft=n/win RAM=` `loadq=n/14`. | | **Body densify height horizon** | `CONTIG_DENSIFY_AHEAD` (64 k past tip) | Safety max walk/receive; primary gate is soft assign (100 MiB free / 1 min confirm window). | | **Confirm feed** | readiness (height/hash), no wire retain | Load pack / lookup-wave caps: [`concurrency.md`](./concurrency.md). Requeue / finish on outcome | @@ -74,7 +74,7 @@ is over target. | Allowed | Forbidden | |---------|-----------| | Limit **densify getdata assign** when BQ payload is over ~100 MiB to heights confirm will consume in the next ~1 min at tip rate | Await a soft gate **before** the next TCP read on a peer | -| At/over 1 GiB assign-stop, densify **holes in the already-fetched height range only** (do not grow past BQ max / lookup_taken) | Drop a body we already received solely for soft budget | +| At/over 1 GiB assign-stop, densify holes only within the ~1 min tip-rate window **and** not past fetched_hi (do not grow past fetched; do not densify far holes outside the window) | Drop a body we already received solely for soft budget | | Free densify ahead while BQ payload is under ~100 MiB | Make healthy peers look stalled by parking the reader on soft backpressure | | Overshoot soft limits while in-flight requests complete; accept all in-flight bodies via `block_queue_offer` (assign-stop is ignored on offer) | Bound process RAM by refusing peer bytes already on the wire |