zakura-network 2.0.0

Networking code for the Zakura node. Internal crate, published to support cargo install zakura
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
use super::{error::*, wire::*, *};

/// Default number of blocks advertised per response.
///
/// Keep block-body ranges narrow so a missing response only holds one height at
/// the body-download floor.
pub const DEFAULT_BS_BLOCKS_PER_RESPONSE: u32 = 1;
/// Default advertised hard cap on concurrent in-flight block requests per peer.
///
/// This is the safety ceiling the BBR-lite cwnd is clamped to, **not** the
/// operating point: the binding per-peer concurrency is the measured
/// bandwidth-delay product (see `DownloadWindow`'s BBR controller), which is
/// normally far below this. In a homogeneous fleet this is the per-peer
/// concurrency ceiling every peer offers.
pub const DEFAULT_BS_MAX_INFLIGHT: u32 = 32000;
/// Initial per-peer cwnd (cold-start point), in blocks.
///
/// The BBR cwnd opens here and converges to the BDP-derived target once the first
/// delivery sample arrives, rather than opening at the full `max_inflight`. This
/// keeps the opening burst modest before a peer's rate/latency is known.
pub const DEFAULT_BS_INITIAL_INFLIGHT: u32 = 64;
/// Maximum peer-advertised in-flight request count accepted by this node.
///
/// This is the hard ceiling the default advertisement ([`DEFAULT_BS_MAX_INFLIGHT`]
/// = 32,000) is clamped to, and also the per-peer outstanding-request safety bound
/// (`EFFECTIVE_BS_OUTBOUND_INFLIGHT_PER_PEER`). It bounds how many concurrent
/// requests a remote peer can make us hold against it, so it doubles as a DoS bound.
pub const MAX_BS_INFLIGHT_REQUESTS: u32 = 32_768;
/// Default total response byte target advertised per range response.
pub const DEFAULT_BS_MAX_RESPONSE_BYTES: u32 = 32 * 1024 * 1024;
/// Default global byte budget for outstanding block-request reservations.
pub const DEFAULT_BS_MAX_INFLIGHT_BLOCK_BYTES: u64 = 6 * 1024 * 1024 * 1024;
/// Worst-case serialized bytes reserved per requested block body.
///
/// Block-sync reserves a per-block size estimate at send time (this worst case
/// when no hint is known) and releases it when the body arrives, so a valid,
/// already-downloaded body is never discarded against the request budget. Each
/// body arrives in its own `Block` frame bounded by [`block::MAX_BLOCK_BYTES`]
/// at decode (`MAX_BS_MESSAGE_BYTES > MAX_BLOCK_BYTES`), so the actual size can
/// never exceed this worst case.
pub const BS_PER_BLOCK_WORST_CASE_BYTES: u64 = block::MAX_BLOCK_BYTES;
/// Default cap on the estimated *resident* memory of the look-ahead pipeline.
///
/// Denominated in resident bytes: admission charges serialized pools (reorder,
/// the applying backlog past the decode window, reservations) at their wire
/// size and the two structurally bounded decoded pools (sequencer input
/// channel, submitted decode window) at `× DESERIALIZED_MEM_FACTOR` (see
/// `admission::estimated_resident_pipeline_bytes`). Because backlog bodies
/// stay serialized, this is (to close approximation) the byte ceiling of the
/// speculative backlog itself, and total process RSS tracks it plus an
/// era-independent overhead — so the default is chosen as a memory target, not
/// derived from the wire budget. 1.5 GiB holds roughly a minute of worst-case
/// committed throughput (~70 MB/s) of look-ahead, nearly two checkpoint ranges
/// of maximum-size blocks, and multiple hours of typical-era backlog: deeper
/// speculation buys nothing the committer can consume.
pub const DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BYTES: u64 = 1536 * 1024 * 1024;
/// Minimum submitted block applies required to resolve one checkpoint range.
///
/// The checkpoint verifier resolves a checkpoint window only after the whole
/// window, including the resolving checkpoint block, is queued. A node that
/// starts one height before a checkpoint-gap boundary can therefore need one
/// maximum checkpoint gap plus the boundary block in flight.
pub const MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES: usize =
    zakura_chain::parameters::checkpoint::constants::MAX_CHECKPOINT_HEIGHT_GAP + 1;
/// The byte size of one full worst-case checkpoint range held in flight.
///
/// The checkpoint verifier resolves a block's commit only once the entire
/// contiguous range to the next checkpoint has been submitted, so the whole
/// range must be co-resident. This floor sizes the resident look-ahead clamp
/// (`clamp_reorder_lookahead_to_floor`); checkpoint-range liveness itself comes
/// from the commit-window admission exemption.
pub const BS_CHECKPOINT_RANGE_BYTE_FLOOR: u64 =
    // `MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES` is `MAX_CHECKPOINT_HEIGHT_GAP + 1`
    // (= 401), which fits `u64` losslessly; the product (~802 MB) cannot overflow.
    MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES as u64 * BS_PER_BLOCK_WORST_CASE_BYTES;
/// Default block-sync request timeout.
pub const DEFAULT_BS_REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
/// Default short leash on a floor (lowest-missing-height) request.
///
/// A floor request that has not been served within this window is rescued to a
/// faster carrier (returned to the queue + the peer retry-avoided), never letting
/// the contiguous download floor wait on a slow peer. Far tighter than the base
/// `request_timeout`, which governs patient above-floor speculation instead.
pub const DEFAULT_BS_FLOOR_RESCUE_TIMEOUT: Duration = Duration::from_secs(2);
/// Request-timeout windows allowed before block-progress liveness disconnects.
const BLOCK_PROGRESS_TIMEOUT_REQUESTS: u32 = 4;
/// Default `GetBlocks` probes sent to a new peer before it proves block-body progress.
pub const DEFAULT_BS_INITIAL_BLOCK_PROBE_REQUESTS: u32 = 1;
/// Maximum `GetBlocks` requests sent to one peer without an accepted block body.
pub const DEFAULT_BS_MAX_REQUESTS_WITHOUT_BLOCK_PROGRESS: u32 = 64;
/// Default cooldown before a no-progress peer may be admitted again.
pub const DEFAULT_BS_NO_PROGRESS_PEER_COOLDOWN: Duration = Duration::from_secs(180);
/// Default hard floor-peer avoid cooldown after a watchdog cancellation.
pub const DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN: Duration = DEFAULT_BS_REQUEST_TIMEOUT;
/// Default block-sync status refresh interval after local frontier changes.
pub const DEFAULT_BS_STATUS_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
/// Default tolerated size-hint deviation percentage before a peer is reported.
pub const DEFAULT_BS_SIZE_DEVIATION_TOLERANCE: u32 = 200;
/// Maximum peer-advertised aggregate byte target accepted per requested range.
///
/// A range response is sent as one `Block` frame per body, and each body frame
/// remains independently bounded by `MAX_BS_MESSAGE_BYTES`. This aggregate cap
/// only controls how many bounded body frames a server sends before `BlocksDone`.
pub const MAX_BS_RESPONSE_BYTES: u32 = DEFAULT_BS_MAX_RESPONSE_BYTES;

/// Default steady-state cwnd gain, percent of the bandwidth-delay product. 300% ramps a
/// proven peer up as `1 → 3 → 9 …`; the reliability discount and delay-gradient ceiling
/// pull it back if the extra concurrency costs drops or standing queue.
pub const DEFAULT_BS_BBR_CWND_GAIN_PERCENT: u32 = 300;
/// Default ProbeBW up-probe pacing gain, percent.
pub const DEFAULT_BS_BBR_PROBE_BW_GAIN_PERCENT: u32 = 125;
/// Default ProbeRTT cadence: how often to drain to re-measure the min-RTT.
pub const DEFAULT_BS_BBR_PROBE_RTT_INTERVAL: Duration = Duration::from_secs(10);
/// Default ProbeRTT hold time at the drained cwnd.
pub const DEFAULT_BS_BBR_PROBE_RTT_DURATION: Duration = Duration::from_millis(200);
/// Default windowed-min horizon for the RTprop (min-RTT) estimate. Kept equal to
/// the ProbeRTT interval so a stale min is always re-probed before it expires.
pub const DEFAULT_BS_BBR_RTPROP_WINDOW: Duration = Duration::from_secs(10);
/// Default max-filter horizon for the BtlBw (delivery-rate) estimate.
pub const DEFAULT_BS_BBR_DELIVERY_RATE_WINDOW: Duration = Duration::from_secs(10);
/// Default per-RTT Startup growth (percent ⇒ ≈2×/RTT exponential ramp).
pub const DEFAULT_BS_BBR_STARTUP_GROWTH_PERCENT: u32 = 200;
/// Default minimum cwnd in blocks — keeps the pipe primed and lets ProbeRTT send.
pub const DEFAULT_BS_BBR_MIN_CWND: u32 = 4;
/// Default minimum cwnd in **bytes**: the [`CwndUnit::Bytes`] floor and the cold-start
/// window before the first delivery sample. Sized to one max block plus headroom (≈2.5 MB)
/// so a worst-case body fits, but a just-proven peer then rides its measured BDP up via the
/// gain instead of bursting to multiple megabytes. Primary live-A/B concurrency lever:
/// raise to push more in flight, lower if floor head-of-line latency regresses.
pub const DEFAULT_BS_BBR_MIN_CWND_BYTES: u64 = block::MAX_BLOCK_BYTES + 512 * 1024;
/// Default delay-gradient down-adjust threshold, percent of RTprop.
pub const DEFAULT_BS_BBR_DELAY_GRADIENT_PERCENT: u32 = 150;
/// Default weight (`0..=100`) with which measured reliability (fraction of requests that
/// deliver a body) discounts the BDP-derived cwnd. `100` = full goodput discount (a peer
/// delivering `r` of its requests holds `r ×` the cwnd); `0` = plain BBR. A dropped
/// request is expensive — it can stall the floor for a whole request-timeout — so the
/// cost is folded into the cwnd rather than ignored.
pub const DEFAULT_BS_BBR_RELIABILITY_WEIGHT_PERCENT: u32 = 100;
/// Default number of slots the floor request may borrow beyond the BBR cwnd, so the
/// lowest missing height is fetched even when every servable peer is at its cwnd.
pub const DEFAULT_BS_FLOOR_BYPASS_SLOTS: u32 = 2;

/// Unit the per-peer BBR cwnd budgets in-flight work against. The controller itself is
/// unit-agnostic (it sizes a cwnd from measured delivery rate × RTprop); the unit only
/// changes how outstanding work is counted against that cwnd in `available_slots`.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CwndUnit {
    /// Count outstanding *requests* against the cwnd (one request ≈ one slot), equal
    /// weight regardless of body size. The A/B baseline — retained for comparison
    /// against the byte controller and for tests.
    Blocks,
    /// Count reserved body *bytes* against the cwnd, where the cwnd is itself a byte
    /// bandwidth-delay product sourced from the header size hints (`BtlBw_bytes ×
    /// RTprop × gain`), so a peer serving large bodies holds fewer in flight and one
    /// serving small bodies holds many. The shipped default.
    #[default]
    Bytes,
}

/// Block-sync peer status advertisement.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct BlockSyncStatus {
    /// Earliest block body this peer can serve.
    pub servable_low: block::Height,
    /// Highest contiguous verified block body this peer can serve.
    pub servable_high: block::Height,
    /// Hash of `servable_high`.
    pub tip_hash: block::Hash,
    /// Maximum blocks the sender will serve per requested range.
    pub max_blocks_per_response: u32,
    /// Maximum concurrent `GetBlocks` requests the sender will service.
    pub max_inflight_requests: u32,
    /// Maximum total response bytes the sender targets per requested range.
    pub max_response_bytes: u32,
}

impl BlockSyncStatus {
    pub(super) fn encode_to<W: Write>(&self, writer: &mut W) -> Result<(), BlockSyncWireError> {
        write_height(writer, self.servable_low)?;
        write_height(writer, self.servable_high)?;
        self.tip_hash.zcash_serialize(&mut *writer)?;
        writer.write_u32::<LittleEndian>(clamp_advertised_blocks(self.max_blocks_per_response))?;
        writer.write_u32::<LittleEndian>(self.max_inflight_requests)?;
        writer.write_u32::<LittleEndian>(self.max_response_bytes.max(1))?;
        Ok(())
    }

    pub(super) fn decode_from<R: Read>(reader: &mut R) -> Result<Self, BlockSyncWireError> {
        Ok(Self {
            servable_low: read_height(reader)?,
            servable_high: read_height(reader)?,
            tip_hash: block::Hash::zcash_deserialize(&mut *reader)?,
            max_blocks_per_response: clamp_advertised_blocks(reader.read_u32::<LittleEndian>()?),
            max_inflight_requests: clamp_advertised_inflight(reader.read_u32::<LittleEndian>()?),
            max_response_bytes: clamp_advertised_response_bytes(reader.read_u32::<LittleEndian>()?),
        })
    }
}

impl Default for BlockSyncStatus {
    fn default() -> Self {
        Self {
            servable_low: block::Height::MIN,
            servable_high: block::Height::MIN,
            tip_hash: block::Hash([0; 32]),
            max_blocks_per_response: DEFAULT_BS_BLOCKS_PER_RESPONSE,
            max_inflight_requests: DEFAULT_BS_MAX_INFLIGHT,
            max_response_bytes: DEFAULT_BS_MAX_RESPONSE_BYTES,
        }
    }
}

/// Block-sync configuration nested under the Zakura P2P-v2 config.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct ZakuraBlockSyncConfig {
    /// Deprecated compatibility key for older rollout configs.
    ///
    /// Zakura block sync is now selected by the top-level `v2_p2p` flag. This
    /// field is accepted but ignored so older configs keep parsing.
    #[doc(hidden)]
    #[serde(
        default,
        skip_serializing,
        deserialize_with = "deserialize_ignored_replace_legacy_syncer"
    )]
    pub replace_legacy_syncer: bool,
    /// Maximum blocks this node advertises per `GetBlocks` response.
    pub max_blocks_per_response: u32,
    /// Maximum concurrent `GetBlocks` requests this node advertises per peer.
    pub max_inflight_requests: u32,
    /// Initial per-peer BBR cwnd (cold-start point), in blocks; converges to the
    /// BDP-derived target once the first delivery is measured.
    pub initial_inflight_requests: u32,
    /// Maximum total response bytes this node advertises per `GetBlocks` response.
    pub max_response_bytes: u32,
    /// Maximum estimated bytes reserved for outstanding block-body requests: a
    /// DoS/pacing bound on in-flight wire data, released at receipt. Received
    /// bodies are bounded by `max_reorder_lookahead_bytes` instead.
    pub max_inflight_block_bytes: u64,
    /// Maximum estimated *resident* memory of look-ahead block bodies retained
    /// by the download pipeline. Serialized backlog and reservations are charged
    /// at wire size; decoded input/submission windows add
    /// `DESERIALIZED_MEM_FACTOR` times their wire size.
    pub max_reorder_lookahead_bytes: u64,
    /// How long to avoid reassigning an expired floor height to the same peer.
    #[serde(with = "humantime_serde")]
    pub floor_peer_avoid_cooldown: Duration,
    /// Depth for block-sync action/body channels, clamped to at least one full
    /// checkpoint range.
    pub max_submitted_block_applies: usize,
    /// Timeout for an outstanding block-body range request.
    #[serde(with = "humantime_serde")]
    pub request_timeout: Duration,
    /// Short leash on a floor request before its height is rescued to a faster
    /// carrier. Clamped positive and never above `request_timeout`.
    #[serde(with = "humantime_serde")]
    pub floor_rescue_timeout: Duration,
    /// How long to keep a peer disconnected after it makes no accepted block progress.
    #[serde(with = "humantime_serde")]
    pub no_progress_peer_cooldown: Duration,
    /// `GetBlocks` requests an unproven peer may receive before its first accepted body,
    /// so a peer that accepts requests but never serves bodies can't spend a full BBR
    /// cold-start burst.
    pub initial_block_probe_requests: u32,
    /// After a peer has proven progress, the cap on requests without an accepted body;
    /// past it the peer gets no more work until it makes progress or the liveness
    /// deadline disconnects it.
    pub max_requests_without_block_progress: u32,
    /// How often this node sends unsolicited status refreshes after local frontier changes.
    #[serde(with = "humantime_serde")]
    pub status_refresh_interval: Duration,
    /// Percentage deviation from advertised body-size hints tolerated before soft scoring.
    pub size_deviation_tolerance: u32,
    /// Steady-state cwnd as a percent of the measured bandwidth-delay product.
    pub bbr_cwnd_gain_percent: u32,
    /// ProbeBW up-probe pacing gain, percent. Reserved: the ProbeBW gain cycle is not
    /// yet wired into the controller, so this knob is currently inert (the BtlBw
    /// max-filter already adopts higher delivery rates without an explicit up-probe).
    pub bbr_probe_bw_gain_percent: u32,
    /// How often to enter ProbeRTT to refresh the min-RTT estimate.
    #[serde(with = "humantime_serde")]
    pub bbr_probe_rtt_interval: Duration,
    /// How long to hold the drained cwnd during ProbeRTT.
    #[serde(with = "humantime_serde")]
    pub bbr_probe_rtt_duration: Duration,
    /// Windowed-min horizon for the RTprop (min-RTT) estimate.
    #[serde(with = "humantime_serde")]
    pub bbr_rtprop_window: Duration,
    /// Max-filter horizon for the delivery-rate (BtlBw) estimate.
    #[serde(with = "humantime_serde")]
    pub bbr_delivery_rate_window: Duration,
    /// Per-RTT Startup cwnd growth, percent. Reserved: there is no separate Startup ramp
    /// phase yet, so this knob is currently inert (cold start uses
    /// `initial_inflight_requests` and the BDP estimate takes over once samples arrive).
    pub bbr_startup_growth_percent: u32,
    /// Minimum cwnd, in blocks (the floor under [`CwndUnit::Blocks`]).
    pub bbr_min_cwnd: u32,
    /// Minimum cwnd, in bytes (the floor under [`CwndUnit::Bytes`]). Doubles as the
    /// cold-start byte window before the first delivery sample, and as the binding
    /// operating window for low-latency peers whose byte-BDP is below it.
    pub bbr_min_cwnd_bytes: u64,
    /// Delay-gradient down-adjust threshold, percent of RTprop.
    pub bbr_delay_gradient_percent: u32,
    /// Weight (`0..=100`) with which measured reliability discounts the BDP-derived cwnd:
    /// `0` = plain BBR, `100` = full goodput discount, so a request-dropping carrier holds
    /// proportionally less in flight. See [`DEFAULT_BS_BBR_RELIABILITY_WEIGHT_PERCENT`].
    pub bbr_reliability_weight_percent: u32,
    /// Unit the BBR cwnd budgets in-flight work against (`bytes` = header-hinted
    /// reserved body bytes, default; `blocks` = request count, the A/B baseline).
    pub bbr_cwnd_unit: CwndUnit,
    /// Slots a floor (lowest-missing-height) request may borrow beyond the BBR cwnd, up
    /// to the peer's advertised hard cap. Lets the floor be fetched even when every
    /// servable peer is saturated at its cwnd; `0` disables the bypass.
    pub floor_bypass_slots: u32,
    /// Block-sync peer caps and queue limits owned by this service.
    pub peer_limits: ServicePeerLimits,
}

fn deserialize_ignored_replace_legacy_syncer<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let _ = bool::deserialize(deserializer)?;
    Ok(false)
}

impl Default for ZakuraBlockSyncConfig {
    fn default() -> Self {
        Self {
            replace_legacy_syncer: false,
            max_blocks_per_response: DEFAULT_BS_BLOCKS_PER_RESPONSE,
            max_inflight_requests: DEFAULT_BS_MAX_INFLIGHT,
            initial_inflight_requests: DEFAULT_BS_INITIAL_INFLIGHT,
            max_response_bytes: DEFAULT_BS_MAX_RESPONSE_BYTES,
            max_inflight_block_bytes: DEFAULT_BS_MAX_INFLIGHT_BLOCK_BYTES,
            max_reorder_lookahead_bytes: DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BYTES,
            floor_peer_avoid_cooldown: DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN,
            max_submitted_block_applies: MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES,
            request_timeout: DEFAULT_BS_REQUEST_TIMEOUT,
            floor_rescue_timeout: DEFAULT_BS_FLOOR_RESCUE_TIMEOUT,
            no_progress_peer_cooldown: DEFAULT_BS_NO_PROGRESS_PEER_COOLDOWN,
            initial_block_probe_requests: DEFAULT_BS_INITIAL_BLOCK_PROBE_REQUESTS,
            max_requests_without_block_progress: DEFAULT_BS_MAX_REQUESTS_WITHOUT_BLOCK_PROGRESS,
            status_refresh_interval: DEFAULT_BS_STATUS_REFRESH_INTERVAL,
            size_deviation_tolerance: DEFAULT_BS_SIZE_DEVIATION_TOLERANCE,
            bbr_cwnd_gain_percent: DEFAULT_BS_BBR_CWND_GAIN_PERCENT,
            bbr_probe_bw_gain_percent: DEFAULT_BS_BBR_PROBE_BW_GAIN_PERCENT,
            bbr_probe_rtt_interval: DEFAULT_BS_BBR_PROBE_RTT_INTERVAL,
            bbr_probe_rtt_duration: DEFAULT_BS_BBR_PROBE_RTT_DURATION,
            bbr_rtprop_window: DEFAULT_BS_BBR_RTPROP_WINDOW,
            bbr_delivery_rate_window: DEFAULT_BS_BBR_DELIVERY_RATE_WINDOW,
            bbr_startup_growth_percent: DEFAULT_BS_BBR_STARTUP_GROWTH_PERCENT,
            bbr_min_cwnd: DEFAULT_BS_BBR_MIN_CWND,
            bbr_min_cwnd_bytes: DEFAULT_BS_BBR_MIN_CWND_BYTES,
            bbr_delay_gradient_percent: DEFAULT_BS_BBR_DELAY_GRADIENT_PERCENT,
            bbr_reliability_weight_percent: DEFAULT_BS_BBR_RELIABILITY_WEIGHT_PERCENT,
            bbr_cwnd_unit: CwndUnit::Bytes,
            floor_bypass_slots: DEFAULT_BS_FLOOR_BYPASS_SLOTS,
            peer_limits: ServicePeerLimits::default(),
        }
    }
}

impl ZakuraBlockSyncConfig {
    /// Return the clamped block-count advertisement for wire status messages.
    pub fn advertised_max_blocks_per_response(&self) -> u32 {
        clamp_advertised_blocks(self.max_blocks_per_response)
    }

    /// Return the locally capped in-flight advertisement for status messages.
    pub fn advertised_max_inflight_requests(&self) -> u32 {
        clamp_advertised_inflight(self.max_inflight_requests)
    }

    /// Return the non-zero response byte advertisement for status messages.
    pub fn advertised_max_response_bytes(&self) -> u32 {
        clamp_advertised_response_bytes(self.max_response_bytes)
    }

    /// Return the non-zero block-sync action/body channel depth.
    pub fn submitted_apply_limit(&self) -> usize {
        self.max_submitted_block_applies
            .max(MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES)
    }

    /// Return the resident look-ahead byte cap.
    ///
    /// Currently the raw configured value; kept as an accessor for the semantic
    /// seam with the clamp interplay. The outstanding-request cap
    /// (`max_inflight_block_bytes`) no longer implies retention, so it must not
    /// silently shrink this memory budget.
    pub fn effective_max_reorder_lookahead_bytes(&self) -> u64 {
        self.max_reorder_lookahead_bytes
    }

    /// Return the floor avoid cooldown clamped to a positive duration.
    pub fn effective_floor_peer_avoid_cooldown(&self) -> Duration {
        self.floor_peer_avoid_cooldown.max(Duration::from_millis(1))
    }

    /// Return the maximum time an active block-sync peer may go without serving
    /// an accepted full block body.
    pub(super) fn effective_liveness_timeout(&self) -> Duration {
        self.request_timeout
            .saturating_mul(BLOCK_PROGRESS_TIMEOUT_REQUESTS)
    }

    /// Return the no-progress peer cooldown clamped to a positive duration.
    pub(super) fn effective_no_progress_peer_cooldown(&self) -> Duration {
        self.no_progress_peer_cooldown.max(Duration::from_millis(1))
    }

    /// Return the floor-rescue leash, clamped positive and no looser than the base
    /// request timeout (a floor request is never more patient than a normal one).
    pub(super) fn effective_floor_rescue_timeout(&self) -> Duration {
        self.floor_rescue_timeout
            .clamp(Duration::from_millis(1), self.request_timeout)
    }

    /// Return the largest byte reservation a single floor request can need.
    pub fn floor_request_byte_reservation(&self) -> u64 {
        let worst_case_blocks = u64::from(self.advertised_max_blocks_per_response())
            .saturating_mul(BS_PER_BLOCK_WORST_CASE_BYTES);
        u64::from(self.advertised_max_response_bytes()).max(worst_case_blocks)
    }

    /// Validate production-safety bounds after deserialization.
    pub fn validate(&self) -> Result<(), &'static str> {
        if self.max_inflight_block_bytes == 0 {
            return Err("max_inflight_block_bytes must be greater than zero");
        }
        if self.max_reorder_lookahead_bytes == 0 {
            return Err("max_reorder_lookahead_bytes must be greater than zero");
        }
        if self.max_inflight_block_bytes <= self.floor_request_byte_reservation() {
            return Err("max_inflight_block_bytes must exceed one floor request");
        }
        if self.request_timeout < Duration::from_millis(1) {
            return Err("request_timeout must be at least 1ms");
        }
        if self.bbr_min_cwnd == 0 {
            return Err("bbr_min_cwnd must be greater than zero");
        }
        if self.bbr_min_cwnd_bytes == 0 {
            return Err("bbr_min_cwnd_bytes must be greater than zero");
        }
        if self.max_requests_without_block_progress == 0 {
            return Err("max_requests_without_block_progress must be greater than zero");
        }
        if self.initial_block_probe_requests == 0 {
            return Err("initial_block_probe_requests must be greater than zero");
        }
        if self.bbr_cwnd_gain_percent < 100
            || self.bbr_probe_bw_gain_percent < 100
            || self.bbr_startup_growth_percent < 100
            || self.bbr_delay_gradient_percent < 100
        {
            return Err("bbr gain/threshold percentages must be at least 100");
        }
        if self.bbr_reliability_weight_percent > 100 {
            return Err("bbr_reliability_weight_percent must be at most 100");
        }
        if self.bbr_probe_rtt_interval <= self.bbr_probe_rtt_duration {
            return Err("bbr_probe_rtt_interval must exceed bbr_probe_rtt_duration");
        }
        Ok(())
    }

    /// Clamp the resident look-ahead budget up to one worst-case checkpoint range
    /// of wire bytes.
    ///
    /// This is defense-in-depth for the speculative above-window lane: checkpoint
    /// sync liveness already comes from the commit-window admission exemption, but
    /// a sub-range byte budget would reject nearly all gated look-ahead work.
    /// Serialized pools are charged at wire size, so the floor is the range's
    /// wire bytes, not its decoded multiple: the bounded decode window's cost is
    /// carried by the exempt lane, which bypasses this budget entirely.
    pub fn clamp_reorder_lookahead_to_floor(&mut self) {
        let range_wire_floor = BS_CHECKPOINT_RANGE_BYTE_FLOOR;
        if self.max_reorder_lookahead_bytes > 0
            && self.max_reorder_lookahead_bytes < range_wire_floor
        {
            tracing::warn!(
                configured_max_reorder_lookahead_bytes = self.max_reorder_lookahead_bytes,
                checkpoint_range_wire_floor = range_wire_floor,
                "zakura.block_sync.max_reorder_lookahead_bytes is below the \
                 checkpoint-range wire floor; clamping it up so checkpoint sync cannot deadlock",
            );
            self.max_reorder_lookahead_bytes = range_wire_floor;
        }
    }

    /// Clamp a positive but sub-floor-request `max_inflight_block_bytes` up to
    /// just above one floor request.
    ///
    /// `validate()` requires the outstanding-request budget to cover at least
    /// one floor request (the bounded floor overdraft repays against it).
    /// Rather than refuse to start — which would break older configs written
    /// when the field also bounded retention and small values were clamped —
    /// raise the budget to the smallest valid value and warn.
    pub fn clamp_inflight_block_bytes_to_request_floor(&mut self) {
        let request_floor = self.floor_request_byte_reservation();
        if self.max_inflight_block_bytes > 0 && self.max_inflight_block_bytes <= request_floor {
            tracing::warn!(
                configured_max_inflight_block_bytes = self.max_inflight_block_bytes,
                floor_request_byte_reservation = request_floor,
                "zakura.block_sync.max_inflight_block_bytes cannot cover one floor \
                 request; clamping it up so the node can start",
            );
            self.max_inflight_block_bytes = request_floor.saturating_add(1);
        }
    }

    /// Build the inert local status used before the block-sync reactor is wired.
    pub fn initial_status(&self) -> BlockSyncStatus {
        BlockSyncStatus {
            max_blocks_per_response: self.advertised_max_blocks_per_response(),
            max_inflight_requests: self.advertised_max_inflight_requests(),
            max_response_bytes: self.advertised_max_response_bytes(),
            ..BlockSyncStatus::default()
        }
    }
}

/// Clamp an advertised block count to the hard stream-6 request cap.
pub fn clamp_advertised_blocks(count: u32) -> u32 {
    count.clamp(1, MAX_BS_BLOCKS_PER_REQUEST)
}

/// Clamp an advertised in-flight request count to the local status ceiling.
pub fn clamp_advertised_inflight(count: u32) -> u32 {
    count.clamp(1, MAX_BS_INFLIGHT_REQUESTS)
}

/// Clamp an advertised response byte target to the largest stream-6 message.
pub fn clamp_advertised_response_bytes(bytes: u32) -> u32 {
    bytes.clamp(1, MAX_BS_RESPONSE_BYTES)
}

/// Maximum inbound `GetBlocks.count` this node will serve before looking at body sizes.
pub fn inbound_get_blocks_count_limit(config: &ZakuraBlockSyncConfig) -> u32 {
    config
        .advertised_max_blocks_per_response()
        .clamp(1, MAX_BS_BLOCKS_PER_REQUEST)
}