Skip to main content

zakura_network/zakura/block_sync/
config.rs

1use super::{error::*, wire::*, *};
2
3/// Default number of blocks advertised per response.
4///
5/// Keep block-body ranges narrow so a missing response only holds one height at
6/// the body-download floor.
7pub const DEFAULT_BS_BLOCKS_PER_RESPONSE: u32 = 1;
8/// Default advertised hard cap on concurrent in-flight block requests per peer.
9///
10/// This is the safety ceiling the BBR-lite cwnd is clamped to, **not** the
11/// operating point: the binding per-peer concurrency is the measured
12/// bandwidth-delay product (see `DownloadWindow`'s BBR controller), which is
13/// normally far below this. In a homogeneous fleet this is the per-peer
14/// concurrency ceiling every peer offers.
15pub const DEFAULT_BS_MAX_INFLIGHT: u32 = 32000;
16/// Initial per-peer cwnd (cold-start point), in blocks.
17///
18/// The BBR cwnd opens here and converges to the BDP-derived target once the first
19/// delivery sample arrives, rather than opening at the full `max_inflight`. This
20/// keeps the opening burst modest before a peer's rate/latency is known.
21pub const DEFAULT_BS_INITIAL_INFLIGHT: u32 = 64;
22/// Maximum peer-advertised in-flight request count accepted by this node.
23///
24/// This is the hard ceiling the default advertisement ([`DEFAULT_BS_MAX_INFLIGHT`]
25/// = 32,000) is clamped to, and also the per-peer outstanding-request safety bound
26/// (`EFFECTIVE_BS_OUTBOUND_INFLIGHT_PER_PEER`). It bounds how many concurrent
27/// requests a remote peer can make us hold against it, so it doubles as a DoS bound.
28pub const MAX_BS_INFLIGHT_REQUESTS: u32 = 32_768;
29/// Default total response byte target advertised per range response.
30pub const DEFAULT_BS_MAX_RESPONSE_BYTES: u32 = 32 * 1024 * 1024;
31/// Default global byte budget for outstanding block-request reservations.
32pub const DEFAULT_BS_MAX_INFLIGHT_BLOCK_BYTES: u64 = 6 * 1024 * 1024 * 1024;
33/// Worst-case serialized bytes reserved per requested block body.
34///
35/// Block-sync reserves a per-block size estimate at send time (this worst case
36/// when no hint is known) and releases it when the body arrives, so a valid,
37/// already-downloaded body is never discarded against the request budget. Each
38/// body arrives in its own `Block` frame bounded by [`block::MAX_BLOCK_BYTES`]
39/// at decode (`MAX_BS_MESSAGE_BYTES > MAX_BLOCK_BYTES`), so the actual size can
40/// never exceed this worst case.
41pub const BS_PER_BLOCK_WORST_CASE_BYTES: u64 = block::MAX_BLOCK_BYTES;
42/// Default cap on the estimated *resident* memory of the look-ahead pipeline.
43///
44/// Denominated in resident bytes: admission charges serialized pools (reorder,
45/// the applying backlog past the decode window, reservations) at their wire
46/// size and the two structurally bounded decoded pools (sequencer input
47/// channel, submitted decode window) at `× DESERIALIZED_MEM_FACTOR` (see
48/// `admission::estimated_resident_pipeline_bytes`). Because backlog bodies
49/// stay serialized, this is (to close approximation) the byte ceiling of the
50/// speculative backlog itself, and total process RSS tracks it plus an
51/// era-independent overhead — so the default is chosen as a memory target, not
52/// derived from the wire budget. 1.5 GiB holds roughly a minute of worst-case
53/// committed throughput (~70 MB/s) of look-ahead, nearly two checkpoint ranges
54/// of maximum-size blocks, and multiple hours of typical-era backlog: deeper
55/// speculation buys nothing the committer can consume.
56pub const DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BYTES: u64 = 1536 * 1024 * 1024;
57/// Minimum submitted block applies required to resolve one checkpoint range.
58///
59/// The checkpoint verifier resolves a checkpoint window only after the whole
60/// window, including the resolving checkpoint block, is queued. A node that
61/// starts one height before a checkpoint-gap boundary can therefore need one
62/// maximum checkpoint gap plus the boundary block in flight.
63pub const MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES: usize =
64    zakura_chain::parameters::checkpoint::constants::MAX_CHECKPOINT_HEIGHT_GAP + 1;
65/// The byte size of one full worst-case checkpoint range held in flight.
66///
67/// The checkpoint verifier resolves a block's commit only once the entire
68/// contiguous range to the next checkpoint has been submitted, so the whole
69/// range must be co-resident. This floor sizes the resident look-ahead clamp
70/// (`clamp_reorder_lookahead_to_floor`); checkpoint-range liveness itself comes
71/// from the commit-window admission exemption.
72pub const BS_CHECKPOINT_RANGE_BYTE_FLOOR: u64 =
73    // `MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES` is `MAX_CHECKPOINT_HEIGHT_GAP + 1`
74    // (= 401), which fits `u64` losslessly; the product (~802 MB) cannot overflow.
75    MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES as u64 * BS_PER_BLOCK_WORST_CASE_BYTES;
76/// Default block-sync request timeout.
77pub const DEFAULT_BS_REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
78/// Default short leash on a floor (lowest-missing-height) request.
79///
80/// A floor request that has not been served within this window is rescued to a
81/// faster carrier (returned to the queue + the peer retry-avoided), never letting
82/// the contiguous download floor wait on a slow peer. Far tighter than the base
83/// `request_timeout`, which governs patient above-floor speculation instead.
84pub const DEFAULT_BS_FLOOR_RESCUE_TIMEOUT: Duration = Duration::from_secs(2);
85/// Request-timeout windows allowed before block-progress liveness disconnects.
86const BLOCK_PROGRESS_TIMEOUT_REQUESTS: u32 = 4;
87/// Default `GetBlocks` probes sent to a new peer before it proves block-body progress.
88pub const DEFAULT_BS_INITIAL_BLOCK_PROBE_REQUESTS: u32 = 1;
89/// Maximum `GetBlocks` requests sent to one peer without an accepted block body.
90pub const DEFAULT_BS_MAX_REQUESTS_WITHOUT_BLOCK_PROGRESS: u32 = 64;
91/// Default cooldown before a no-progress peer may be admitted again.
92pub const DEFAULT_BS_NO_PROGRESS_PEER_COOLDOWN: Duration = Duration::from_secs(180);
93/// Default hard floor-peer avoid cooldown after a watchdog cancellation.
94pub const DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN: Duration = DEFAULT_BS_REQUEST_TIMEOUT;
95/// Default block-sync status refresh interval after local frontier changes.
96pub const DEFAULT_BS_STATUS_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
97/// Default tolerated size-hint deviation percentage before a peer is reported.
98pub const DEFAULT_BS_SIZE_DEVIATION_TOLERANCE: u32 = 200;
99/// Maximum peer-advertised aggregate byte target accepted per requested range.
100///
101/// A range response is sent as one `Block` frame per body, and each body frame
102/// remains independently bounded by `MAX_BS_MESSAGE_BYTES`. This aggregate cap
103/// only controls how many bounded body frames a server sends before `BlocksDone`.
104pub const MAX_BS_RESPONSE_BYTES: u32 = DEFAULT_BS_MAX_RESPONSE_BYTES;
105
106/// Default steady-state cwnd gain, percent of the bandwidth-delay product. 300% ramps a
107/// proven peer up as `1 → 3 → 9 …`; the reliability discount and delay-gradient ceiling
108/// pull it back if the extra concurrency costs drops or standing queue.
109pub const DEFAULT_BS_BBR_CWND_GAIN_PERCENT: u32 = 300;
110/// Default ProbeBW up-probe pacing gain, percent.
111pub const DEFAULT_BS_BBR_PROBE_BW_GAIN_PERCENT: u32 = 125;
112/// Default ProbeRTT cadence: how often to drain to re-measure the min-RTT.
113pub const DEFAULT_BS_BBR_PROBE_RTT_INTERVAL: Duration = Duration::from_secs(10);
114/// Default ProbeRTT hold time at the drained cwnd.
115pub const DEFAULT_BS_BBR_PROBE_RTT_DURATION: Duration = Duration::from_millis(200);
116/// Default windowed-min horizon for the RTprop (min-RTT) estimate. Kept equal to
117/// the ProbeRTT interval so a stale min is always re-probed before it expires.
118pub const DEFAULT_BS_BBR_RTPROP_WINDOW: Duration = Duration::from_secs(10);
119/// Default max-filter horizon for the BtlBw (delivery-rate) estimate.
120pub const DEFAULT_BS_BBR_DELIVERY_RATE_WINDOW: Duration = Duration::from_secs(10);
121/// Default per-RTT Startup growth (percent ⇒ ≈2×/RTT exponential ramp).
122pub const DEFAULT_BS_BBR_STARTUP_GROWTH_PERCENT: u32 = 200;
123/// Default minimum cwnd in blocks — keeps the pipe primed and lets ProbeRTT send.
124pub const DEFAULT_BS_BBR_MIN_CWND: u32 = 4;
125/// Default minimum cwnd in **bytes**: the [`CwndUnit::Bytes`] floor and the cold-start
126/// window before the first delivery sample. Sized to one max block plus headroom (≈2.5 MB)
127/// so a worst-case body fits, but a just-proven peer then rides its measured BDP up via the
128/// gain instead of bursting to multiple megabytes. Primary live-A/B concurrency lever:
129/// raise to push more in flight, lower if floor head-of-line latency regresses.
130pub const DEFAULT_BS_BBR_MIN_CWND_BYTES: u64 = block::MAX_BLOCK_BYTES + 512 * 1024;
131/// Default delay-gradient down-adjust threshold, percent of RTprop.
132pub const DEFAULT_BS_BBR_DELAY_GRADIENT_PERCENT: u32 = 150;
133/// Default weight (`0..=100`) with which measured reliability (fraction of requests that
134/// deliver a body) discounts the BDP-derived cwnd. `100` = full goodput discount (a peer
135/// delivering `r` of its requests holds `r ×` the cwnd); `0` = plain BBR. A dropped
136/// request is expensive — it can stall the floor for a whole request-timeout — so the
137/// cost is folded into the cwnd rather than ignored.
138pub const DEFAULT_BS_BBR_RELIABILITY_WEIGHT_PERCENT: u32 = 100;
139/// Default number of slots the floor request may borrow beyond the BBR cwnd, so the
140/// lowest missing height is fetched even when every servable peer is at its cwnd.
141pub const DEFAULT_BS_FLOOR_BYPASS_SLOTS: u32 = 2;
142
143/// Unit the per-peer BBR cwnd budgets in-flight work against. The controller itself is
144/// unit-agnostic (it sizes a cwnd from measured delivery rate × RTprop); the unit only
145/// changes how outstanding work is counted against that cwnd in `available_slots`.
146#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum CwndUnit {
149    /// Count outstanding *requests* against the cwnd (one request ≈ one slot), equal
150    /// weight regardless of body size. The A/B baseline — retained for comparison
151    /// against the byte controller and for tests.
152    Blocks,
153    /// Count reserved body *bytes* against the cwnd, where the cwnd is itself a byte
154    /// bandwidth-delay product sourced from the header size hints (`BtlBw_bytes ×
155    /// RTprop × gain`), so a peer serving large bodies holds fewer in flight and one
156    /// serving small bodies holds many. The shipped default.
157    #[default]
158    Bytes,
159}
160
161/// Block-sync peer status advertisement.
162#[derive(Copy, Clone, Debug, Eq, PartialEq)]
163pub struct BlockSyncStatus {
164    /// Earliest block body this peer can serve.
165    pub servable_low: block::Height,
166    /// Highest contiguous verified block body this peer can serve.
167    pub servable_high: block::Height,
168    /// Hash of `servable_high`.
169    pub tip_hash: block::Hash,
170    /// Maximum blocks the sender will serve per requested range.
171    pub max_blocks_per_response: u32,
172    /// Maximum concurrent `GetBlocks` requests the sender will service.
173    pub max_inflight_requests: u32,
174    /// Maximum total response bytes the sender targets per requested range.
175    pub max_response_bytes: u32,
176}
177
178impl BlockSyncStatus {
179    pub(super) fn encode_to<W: Write>(&self, writer: &mut W) -> Result<(), BlockSyncWireError> {
180        write_height(writer, self.servable_low)?;
181        write_height(writer, self.servable_high)?;
182        self.tip_hash.zcash_serialize(&mut *writer)?;
183        writer.write_u32::<LittleEndian>(clamp_advertised_blocks(self.max_blocks_per_response))?;
184        writer.write_u32::<LittleEndian>(self.max_inflight_requests)?;
185        writer.write_u32::<LittleEndian>(self.max_response_bytes.max(1))?;
186        Ok(())
187    }
188
189    pub(super) fn decode_from<R: Read>(reader: &mut R) -> Result<Self, BlockSyncWireError> {
190        Ok(Self {
191            servable_low: read_height(reader)?,
192            servable_high: read_height(reader)?,
193            tip_hash: block::Hash::zcash_deserialize(&mut *reader)?,
194            max_blocks_per_response: clamp_advertised_blocks(reader.read_u32::<LittleEndian>()?),
195            max_inflight_requests: clamp_advertised_inflight(reader.read_u32::<LittleEndian>()?),
196            max_response_bytes: clamp_advertised_response_bytes(reader.read_u32::<LittleEndian>()?),
197        })
198    }
199}
200
201impl Default for BlockSyncStatus {
202    fn default() -> Self {
203        Self {
204            servable_low: block::Height::MIN,
205            servable_high: block::Height::MIN,
206            tip_hash: block::Hash([0; 32]),
207            max_blocks_per_response: DEFAULT_BS_BLOCKS_PER_RESPONSE,
208            max_inflight_requests: DEFAULT_BS_MAX_INFLIGHT,
209            max_response_bytes: DEFAULT_BS_MAX_RESPONSE_BYTES,
210        }
211    }
212}
213
214/// Block-sync configuration nested under the Zakura P2P-v2 config.
215#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields, default)]
217pub struct ZakuraBlockSyncConfig {
218    /// Deprecated compatibility key for older rollout configs.
219    ///
220    /// Zakura block sync is now selected by the top-level `v2_p2p` flag. This
221    /// field is accepted but ignored so older configs keep parsing.
222    #[doc(hidden)]
223    #[serde(
224        default,
225        skip_serializing,
226        deserialize_with = "deserialize_ignored_replace_legacy_syncer"
227    )]
228    pub replace_legacy_syncer: bool,
229    /// Maximum blocks this node advertises per `GetBlocks` response.
230    pub max_blocks_per_response: u32,
231    /// Maximum concurrent `GetBlocks` requests this node advertises per peer.
232    pub max_inflight_requests: u32,
233    /// Initial per-peer BBR cwnd (cold-start point), in blocks; converges to the
234    /// BDP-derived target once the first delivery is measured.
235    pub initial_inflight_requests: u32,
236    /// Maximum total response bytes this node advertises per `GetBlocks` response.
237    pub max_response_bytes: u32,
238    /// Maximum estimated bytes reserved for outstanding block-body requests: a
239    /// DoS/pacing bound on in-flight wire data, released at receipt. Received
240    /// bodies are bounded by `max_reorder_lookahead_bytes` instead.
241    pub max_inflight_block_bytes: u64,
242    /// Maximum estimated *resident* memory of look-ahead block bodies retained
243    /// by the download pipeline. Serialized backlog and reservations are charged
244    /// at wire size; decoded input/submission windows add
245    /// `DESERIALIZED_MEM_FACTOR` times their wire size.
246    pub max_reorder_lookahead_bytes: u64,
247    /// How long to avoid reassigning an expired floor height to the same peer.
248    #[serde(with = "humantime_serde")]
249    pub floor_peer_avoid_cooldown: Duration,
250    /// Depth for block-sync action/body channels, clamped to at least one full
251    /// checkpoint range.
252    pub max_submitted_block_applies: usize,
253    /// Timeout for an outstanding block-body range request.
254    #[serde(with = "humantime_serde")]
255    pub request_timeout: Duration,
256    /// Short leash on a floor request before its height is rescued to a faster
257    /// carrier. Clamped positive and never above `request_timeout`.
258    #[serde(with = "humantime_serde")]
259    pub floor_rescue_timeout: Duration,
260    /// How long to keep a peer disconnected after it makes no accepted block progress.
261    #[serde(with = "humantime_serde")]
262    pub no_progress_peer_cooldown: Duration,
263    /// `GetBlocks` requests an unproven peer may receive before its first accepted body,
264    /// so a peer that accepts requests but never serves bodies can't spend a full BBR
265    /// cold-start burst.
266    pub initial_block_probe_requests: u32,
267    /// After a peer has proven progress, the cap on requests without an accepted body;
268    /// past it the peer gets no more work until it makes progress or the liveness
269    /// deadline disconnects it.
270    pub max_requests_without_block_progress: u32,
271    /// How often this node sends unsolicited status refreshes after local frontier changes.
272    #[serde(with = "humantime_serde")]
273    pub status_refresh_interval: Duration,
274    /// Percentage deviation from advertised body-size hints tolerated before soft scoring.
275    pub size_deviation_tolerance: u32,
276    /// Steady-state cwnd as a percent of the measured bandwidth-delay product.
277    pub bbr_cwnd_gain_percent: u32,
278    /// ProbeBW up-probe pacing gain, percent. Reserved: the ProbeBW gain cycle is not
279    /// yet wired into the controller, so this knob is currently inert (the BtlBw
280    /// max-filter already adopts higher delivery rates without an explicit up-probe).
281    pub bbr_probe_bw_gain_percent: u32,
282    /// How often to enter ProbeRTT to refresh the min-RTT estimate.
283    #[serde(with = "humantime_serde")]
284    pub bbr_probe_rtt_interval: Duration,
285    /// How long to hold the drained cwnd during ProbeRTT.
286    #[serde(with = "humantime_serde")]
287    pub bbr_probe_rtt_duration: Duration,
288    /// Windowed-min horizon for the RTprop (min-RTT) estimate.
289    #[serde(with = "humantime_serde")]
290    pub bbr_rtprop_window: Duration,
291    /// Max-filter horizon for the delivery-rate (BtlBw) estimate.
292    #[serde(with = "humantime_serde")]
293    pub bbr_delivery_rate_window: Duration,
294    /// Per-RTT Startup cwnd growth, percent. Reserved: there is no separate Startup ramp
295    /// phase yet, so this knob is currently inert (cold start uses
296    /// `initial_inflight_requests` and the BDP estimate takes over once samples arrive).
297    pub bbr_startup_growth_percent: u32,
298    /// Minimum cwnd, in blocks (the floor under [`CwndUnit::Blocks`]).
299    pub bbr_min_cwnd: u32,
300    /// Minimum cwnd, in bytes (the floor under [`CwndUnit::Bytes`]). Doubles as the
301    /// cold-start byte window before the first delivery sample, and as the binding
302    /// operating window for low-latency peers whose byte-BDP is below it.
303    pub bbr_min_cwnd_bytes: u64,
304    /// Delay-gradient down-adjust threshold, percent of RTprop.
305    pub bbr_delay_gradient_percent: u32,
306    /// Weight (`0..=100`) with which measured reliability discounts the BDP-derived cwnd:
307    /// `0` = plain BBR, `100` = full goodput discount, so a request-dropping carrier holds
308    /// proportionally less in flight. See [`DEFAULT_BS_BBR_RELIABILITY_WEIGHT_PERCENT`].
309    pub bbr_reliability_weight_percent: u32,
310    /// Unit the BBR cwnd budgets in-flight work against (`bytes` = header-hinted
311    /// reserved body bytes, default; `blocks` = request count, the A/B baseline).
312    pub bbr_cwnd_unit: CwndUnit,
313    /// Slots a floor (lowest-missing-height) request may borrow beyond the BBR cwnd, up
314    /// to the peer's advertised hard cap. Lets the floor be fetched even when every
315    /// servable peer is saturated at its cwnd; `0` disables the bypass.
316    pub floor_bypass_slots: u32,
317    /// Block-sync peer caps and queue limits owned by this service.
318    pub peer_limits: ServicePeerLimits,
319}
320
321fn deserialize_ignored_replace_legacy_syncer<'de, D>(deserializer: D) -> Result<bool, D::Error>
322where
323    D: serde::Deserializer<'de>,
324{
325    let _ = bool::deserialize(deserializer)?;
326    Ok(false)
327}
328
329impl Default for ZakuraBlockSyncConfig {
330    fn default() -> Self {
331        Self {
332            replace_legacy_syncer: false,
333            max_blocks_per_response: DEFAULT_BS_BLOCKS_PER_RESPONSE,
334            max_inflight_requests: DEFAULT_BS_MAX_INFLIGHT,
335            initial_inflight_requests: DEFAULT_BS_INITIAL_INFLIGHT,
336            max_response_bytes: DEFAULT_BS_MAX_RESPONSE_BYTES,
337            max_inflight_block_bytes: DEFAULT_BS_MAX_INFLIGHT_BLOCK_BYTES,
338            max_reorder_lookahead_bytes: DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BYTES,
339            floor_peer_avoid_cooldown: DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN,
340            max_submitted_block_applies: MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES,
341            request_timeout: DEFAULT_BS_REQUEST_TIMEOUT,
342            floor_rescue_timeout: DEFAULT_BS_FLOOR_RESCUE_TIMEOUT,
343            no_progress_peer_cooldown: DEFAULT_BS_NO_PROGRESS_PEER_COOLDOWN,
344            initial_block_probe_requests: DEFAULT_BS_INITIAL_BLOCK_PROBE_REQUESTS,
345            max_requests_without_block_progress: DEFAULT_BS_MAX_REQUESTS_WITHOUT_BLOCK_PROGRESS,
346            status_refresh_interval: DEFAULT_BS_STATUS_REFRESH_INTERVAL,
347            size_deviation_tolerance: DEFAULT_BS_SIZE_DEVIATION_TOLERANCE,
348            bbr_cwnd_gain_percent: DEFAULT_BS_BBR_CWND_GAIN_PERCENT,
349            bbr_probe_bw_gain_percent: DEFAULT_BS_BBR_PROBE_BW_GAIN_PERCENT,
350            bbr_probe_rtt_interval: DEFAULT_BS_BBR_PROBE_RTT_INTERVAL,
351            bbr_probe_rtt_duration: DEFAULT_BS_BBR_PROBE_RTT_DURATION,
352            bbr_rtprop_window: DEFAULT_BS_BBR_RTPROP_WINDOW,
353            bbr_delivery_rate_window: DEFAULT_BS_BBR_DELIVERY_RATE_WINDOW,
354            bbr_startup_growth_percent: DEFAULT_BS_BBR_STARTUP_GROWTH_PERCENT,
355            bbr_min_cwnd: DEFAULT_BS_BBR_MIN_CWND,
356            bbr_min_cwnd_bytes: DEFAULT_BS_BBR_MIN_CWND_BYTES,
357            bbr_delay_gradient_percent: DEFAULT_BS_BBR_DELAY_GRADIENT_PERCENT,
358            bbr_reliability_weight_percent: DEFAULT_BS_BBR_RELIABILITY_WEIGHT_PERCENT,
359            bbr_cwnd_unit: CwndUnit::Bytes,
360            floor_bypass_slots: DEFAULT_BS_FLOOR_BYPASS_SLOTS,
361            peer_limits: ServicePeerLimits::default(),
362        }
363    }
364}
365
366impl ZakuraBlockSyncConfig {
367    /// Return the clamped block-count advertisement for wire status messages.
368    pub fn advertised_max_blocks_per_response(&self) -> u32 {
369        clamp_advertised_blocks(self.max_blocks_per_response)
370    }
371
372    /// Return the locally capped in-flight advertisement for status messages.
373    pub fn advertised_max_inflight_requests(&self) -> u32 {
374        clamp_advertised_inflight(self.max_inflight_requests)
375    }
376
377    /// Return the non-zero response byte advertisement for status messages.
378    pub fn advertised_max_response_bytes(&self) -> u32 {
379        clamp_advertised_response_bytes(self.max_response_bytes)
380    }
381
382    /// Return the non-zero block-sync action/body channel depth.
383    pub fn submitted_apply_limit(&self) -> usize {
384        self.max_submitted_block_applies
385            .max(MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES)
386    }
387
388    /// Return the resident look-ahead byte cap.
389    ///
390    /// Currently the raw configured value; kept as an accessor for the semantic
391    /// seam with the clamp interplay. The outstanding-request cap
392    /// (`max_inflight_block_bytes`) no longer implies retention, so it must not
393    /// silently shrink this memory budget.
394    pub fn effective_max_reorder_lookahead_bytes(&self) -> u64 {
395        self.max_reorder_lookahead_bytes
396    }
397
398    /// Return the floor avoid cooldown clamped to a positive duration.
399    pub fn effective_floor_peer_avoid_cooldown(&self) -> Duration {
400        self.floor_peer_avoid_cooldown.max(Duration::from_millis(1))
401    }
402
403    /// Return the maximum time an active block-sync peer may go without serving
404    /// an accepted full block body.
405    pub(super) fn effective_liveness_timeout(&self) -> Duration {
406        self.request_timeout
407            .saturating_mul(BLOCK_PROGRESS_TIMEOUT_REQUESTS)
408    }
409
410    /// Return the no-progress peer cooldown clamped to a positive duration.
411    pub(super) fn effective_no_progress_peer_cooldown(&self) -> Duration {
412        self.no_progress_peer_cooldown.max(Duration::from_millis(1))
413    }
414
415    /// Return the floor-rescue leash, clamped positive and no looser than the base
416    /// request timeout (a floor request is never more patient than a normal one).
417    pub(super) fn effective_floor_rescue_timeout(&self) -> Duration {
418        self.floor_rescue_timeout
419            .clamp(Duration::from_millis(1), self.request_timeout)
420    }
421
422    /// Return the largest byte reservation a single floor request can need.
423    pub fn floor_request_byte_reservation(&self) -> u64 {
424        let worst_case_blocks = u64::from(self.advertised_max_blocks_per_response())
425            .saturating_mul(BS_PER_BLOCK_WORST_CASE_BYTES);
426        u64::from(self.advertised_max_response_bytes()).max(worst_case_blocks)
427    }
428
429    /// Validate production-safety bounds after deserialization.
430    pub fn validate(&self) -> Result<(), &'static str> {
431        if self.max_inflight_block_bytes == 0 {
432            return Err("max_inflight_block_bytes must be greater than zero");
433        }
434        if self.max_reorder_lookahead_bytes == 0 {
435            return Err("max_reorder_lookahead_bytes must be greater than zero");
436        }
437        if self.max_inflight_block_bytes <= self.floor_request_byte_reservation() {
438            return Err("max_inflight_block_bytes must exceed one floor request");
439        }
440        if self.request_timeout < Duration::from_millis(1) {
441            return Err("request_timeout must be at least 1ms");
442        }
443        if self.bbr_min_cwnd == 0 {
444            return Err("bbr_min_cwnd must be greater than zero");
445        }
446        if self.bbr_min_cwnd_bytes == 0 {
447            return Err("bbr_min_cwnd_bytes must be greater than zero");
448        }
449        if self.max_requests_without_block_progress == 0 {
450            return Err("max_requests_without_block_progress must be greater than zero");
451        }
452        if self.initial_block_probe_requests == 0 {
453            return Err("initial_block_probe_requests must be greater than zero");
454        }
455        if self.bbr_cwnd_gain_percent < 100
456            || self.bbr_probe_bw_gain_percent < 100
457            || self.bbr_startup_growth_percent < 100
458            || self.bbr_delay_gradient_percent < 100
459        {
460            return Err("bbr gain/threshold percentages must be at least 100");
461        }
462        if self.bbr_reliability_weight_percent > 100 {
463            return Err("bbr_reliability_weight_percent must be at most 100");
464        }
465        if self.bbr_probe_rtt_interval <= self.bbr_probe_rtt_duration {
466            return Err("bbr_probe_rtt_interval must exceed bbr_probe_rtt_duration");
467        }
468        Ok(())
469    }
470
471    /// Clamp the resident look-ahead budget up to one worst-case checkpoint range
472    /// of wire bytes.
473    ///
474    /// This is defense-in-depth for the speculative above-window lane: checkpoint
475    /// sync liveness already comes from the commit-window admission exemption, but
476    /// a sub-range byte budget would reject nearly all gated look-ahead work.
477    /// Serialized pools are charged at wire size, so the floor is the range's
478    /// wire bytes, not its decoded multiple: the bounded decode window's cost is
479    /// carried by the exempt lane, which bypasses this budget entirely.
480    pub fn clamp_reorder_lookahead_to_floor(&mut self) {
481        let range_wire_floor = BS_CHECKPOINT_RANGE_BYTE_FLOOR;
482        if self.max_reorder_lookahead_bytes > 0
483            && self.max_reorder_lookahead_bytes < range_wire_floor
484        {
485            tracing::warn!(
486                configured_max_reorder_lookahead_bytes = self.max_reorder_lookahead_bytes,
487                checkpoint_range_wire_floor = range_wire_floor,
488                "zakura.block_sync.max_reorder_lookahead_bytes is below the \
489                 checkpoint-range wire floor; clamping it up so checkpoint sync cannot deadlock",
490            );
491            self.max_reorder_lookahead_bytes = range_wire_floor;
492        }
493    }
494
495    /// Clamp a positive but sub-floor-request `max_inflight_block_bytes` up to
496    /// just above one floor request.
497    ///
498    /// `validate()` requires the outstanding-request budget to cover at least
499    /// one floor request (the bounded floor overdraft repays against it).
500    /// Rather than refuse to start — which would break older configs written
501    /// when the field also bounded retention and small values were clamped —
502    /// raise the budget to the smallest valid value and warn.
503    pub fn clamp_inflight_block_bytes_to_request_floor(&mut self) {
504        let request_floor = self.floor_request_byte_reservation();
505        if self.max_inflight_block_bytes > 0 && self.max_inflight_block_bytes <= request_floor {
506            tracing::warn!(
507                configured_max_inflight_block_bytes = self.max_inflight_block_bytes,
508                floor_request_byte_reservation = request_floor,
509                "zakura.block_sync.max_inflight_block_bytes cannot cover one floor \
510                 request; clamping it up so the node can start",
511            );
512            self.max_inflight_block_bytes = request_floor.saturating_add(1);
513        }
514    }
515
516    /// Build the inert local status used before the block-sync reactor is wired.
517    pub fn initial_status(&self) -> BlockSyncStatus {
518        BlockSyncStatus {
519            max_blocks_per_response: self.advertised_max_blocks_per_response(),
520            max_inflight_requests: self.advertised_max_inflight_requests(),
521            max_response_bytes: self.advertised_max_response_bytes(),
522            ..BlockSyncStatus::default()
523        }
524    }
525}
526
527/// Clamp an advertised block count to the hard stream-6 request cap.
528pub fn clamp_advertised_blocks(count: u32) -> u32 {
529    count.clamp(1, MAX_BS_BLOCKS_PER_REQUEST)
530}
531
532/// Clamp an advertised in-flight request count to the local status ceiling.
533pub fn clamp_advertised_inflight(count: u32) -> u32 {
534    count.clamp(1, MAX_BS_INFLIGHT_REQUESTS)
535}
536
537/// Clamp an advertised response byte target to the largest stream-6 message.
538pub fn clamp_advertised_response_bytes(bytes: u32) -> u32 {
539    bytes.clamp(1, MAX_BS_RESPONSE_BYTES)
540}
541
542/// Maximum inbound `GetBlocks.count` this node will serve before looking at body sizes.
543pub fn inbound_get_blocks_count_limit(config: &ZakuraBlockSyncConfig) -> u32 {
544    config
545        .advertised_max_blocks_per_response()
546        .clamp(1, MAX_BS_BLOCKS_PER_REQUEST)
547}