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}