From bfdfe7607eef905dc3be2ce863f154b2282d944c Mon Sep 17 00:00:00 2001 From: evan-forbes Date: Sat, 27 Jun 2026 20:43:16 -0500 Subject: [PATCH] fix(network): block-sync stack review follow-ups Follow-ups from review of the merged block-sync throughput stack (#284, #286, #287, #288, #289): - sequencer: drop the redundant 500ms timer-driven floor-starvation shed. Floor rescue is already demand-driven (inline after each accepted body, plus the synchronous FundFloorReservation path), so the periodic backstop was a pure fixed-cadence poll. - reactor: make the floor watchdog event-driven. Arm a sleep to the earliest outstanding floor-claim deadline instead of polling on a fixed tick, mirroring the per-peer routine's own-timeout arm. Removes the floor_watchdog_tick config knob. - consensus/state: rename consensus.disable_vct_fast_sync -> consensus.vct_fast_sync (default true) to remove the double negative. Mirror updated in state config, docs, and CHANGELOG. - work_queue: debug_assert the take_in_range_budgeted precondition (positive count, low <= high) instead of silently returning empty. - block_sync: reword refactor-historical comments ("ported from", "verbatim", "matches the previous", "used to") to describe current behavior. AI-assisted: implemented with Claude Code (Opus 4.8). --- CHANGELOG.md | 5 +- docs/design/verified-commitment-trees.md | 34 ++--- docs/plans/headersync_roots_review.md | 2 +- zebra-consensus/src/config.rs | 40 +++--- zebra-network/src/zakura/block_sync/config.rs | 13 -- zebra-network/src/zakura/block_sync/events.rs | 4 +- .../src/zakura/block_sync/peer_registry.rs | 21 ++- .../src/zakura/block_sync/peer_routine.rs | 114 +++++++---------- .../src/zakura/block_sync/reactor.rs | 48 ++++--- .../src/zakura/block_sync/sequencer.rs | 4 +- .../src/zakura/block_sync/sequencer_task.rs | 121 ++++++++---------- zebra-network/src/zakura/block_sync/state.rs | 12 +- zebra-network/src/zakura/block_sync/tests.rs | 14 +- .../src/zakura/block_sync/work_queue.rs | 15 ++- zebra-state/src/config.rs | 22 ++-- zebra-state/src/service/finalized_state.rs | 4 +- .../src/service/finalized_state/tests/prop.rs | 6 +- .../src/service/finalized_state/vct.rs | 50 ++++---- .../zebra_db/block/tests/prune.rs | 6 +- zebrad/src/commands/start.rs | 2 +- zebrad/tests/common/configs/v5.0.0-rc.3.toml | 3 +- zebrad/tests/common/configs/v5.0.0-rc.4.toml | 3 +- 22 files changed, 261 insertions(+), 282 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b30ee800563..87d74086f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,8 +112,9 @@ and this project adheres to [Semantic Versioning](https://semver.org). no untrusted data can influence consensus state. This is the default whenever `consensus.checkpoint_sync = true` on a network with an embedded handoff frontier (Mainnet), for both Archive and Pruned storage modes. The new - `consensus.disable_vct_fast_sync` flag (default `false`) keeps checkpoint sync - enabled while forcing the legacy per-block recompute. Bumps the state database + `consensus.vct_fast_sync` flag (default `true`) selects this fast path; set it to + `false` to keep checkpoint sync enabled while forcing the legacy per-block + recompute. Bumps the state database format to 27.3.0 (new column families only; no data migration). - Include the `zebra-rollback-state` and `zebra-prune-state` utilities alongside `zebrad` in release Docker images and Docker CI builds. diff --git a/docs/design/verified-commitment-trees.md b/docs/design/verified-commitment-trees.md index 44bed078710..67b24a7ce5b 100644 --- a/docs/design/verified-commitment-trees.md +++ b/docs/design/verified-commitment-trees.md @@ -67,7 +67,7 @@ the direct below-Heartwood/below-NU5 checks); fold it in; freeze the frontier ( | **Fail closed** | In the frozen window, refuse the commit (retryable) rather than recompute or guess (§8). | | **Provisional roots** | Peer-supplied roots carried in the header-sync `Headers` message and persisted to `zakura_header_commitment_roots_by_height` ahead of body commit. Advisory until verify-before-commit authenticates them (§4.2, §6). | | **All-or-nothing** | A `Headers` message carries roots for _every_ header in the range or none; a partial root set is rejected on the wire and never served (§5.4). | -| **Kill switch** | `consensus.disable_vct_fast_sync = true`: keep checkpoint sync but force the legacy committer (§4.4). | +| **Kill switch** | `consensus.vct_fast_sync = false`: keep checkpoint sync but force the legacy committer (§4.4). | For where each piece lives in the tree, see the file map (§15). @@ -97,10 +97,10 @@ or verify a root falls back to the legacy recompute, bit-identical to today. fast-synced database format. - **Not a consensus change.** There are exactly two enduring code paths: the standard local tree rebuild (legacy) and the fast verified path. Which one runs is config-driven by - `consensus.checkpoint_sync` plus the rollout force-disable knob - (`consensus.disable_vct_fast_sync`; §4.4); the `state.storage_mode` axis (Archive vs. Pruned) + `consensus.checkpoint_sync` plus the rollout fast-sync knob + (`consensus.vct_fast_sync`; §4.4); the `state.storage_mode` axis (Archive vs. Pruned) is orthogonal — it controls raw-tx/index pruning, not the tree path, so both storage modes - use the fast path under checkpoint sync unless force-disabled. The network `PeerSource` and + use the fast path under checkpoint sync unless fast sync is disabled. The network `PeerSource` and crate-local test fixtures are _sources_ behind one seam (§5.3) — not modes. - **No new cryptography.** Verification reuses the existing consensus checks (`block_commitment_is_valid_for_chain_history`, `HistoryTree::push`); see §6. @@ -178,15 +178,15 @@ precedence once the corresponding body is durable. ### 4.4 Mode selection: fast under checkpoint sync The fast-vs-legacy choice is driven by user-facing config, not by env vars. The axes are -`consensus.checkpoint_sync` (full checkpoint trust), `consensus.disable_vct_fast_sync` (initial -rollout force-disable for VCT fast sync), and `state.storage_mode` (Archive vs. Pruned, an +`consensus.checkpoint_sync` (full checkpoint trust), `consensus.vct_fast_sync` (initial +rollout fast-sync knob for VCT fast sync), and `state.storage_mode` (Archive vs. Pruned, an orthogonal pruning axis). The resulting modes: | Mode | Config | Tree behavior | | --- | --- | --- | -| **Archive** (default) | `consensus.checkpoint_sync = true`, `consensus.disable_vct_fast_sync = false`, `storage_mode = archive` | Fast — verified roots folded in, recompute skipped. Unpruned (raw tx + indexes kept). No per-height tree history below the last checkpoint height _for now_ (§7, §10). | -| **Pruning** | `consensus.checkpoint_sync = true`, `consensus.disable_vct_fast_sync = false`, `storage_mode.pruned` | Fast — same as Archive, **plus** raw-tx/index pruning outside the retention window. | -| **Force-disabled VCT** | `consensus.checkpoint_sync = true`, `consensus.disable_vct_fast_sync = true` (any storage mode) | Legacy — keeps checkpoint sync enabled but fully reconstructs the Sapling/Orchard trees per block. | +| **Archive** (default) | `consensus.checkpoint_sync = true`, `consensus.vct_fast_sync = true`, `storage_mode = archive` | Fast — verified roots folded in, recompute skipped. Unpruned (raw tx + indexes kept). No per-height tree history below the last checkpoint height _for now_ (§7, §10). | +| **Pruning** | `consensus.checkpoint_sync = true`, `consensus.vct_fast_sync = true`, `storage_mode.pruned` | Fast — same as Archive, **plus** raw-tx/index pruning outside the retention window. | +| **Force-disabled VCT** | `consensus.checkpoint_sync = true`, `consensus.vct_fast_sync = false` (any storage mode) | Legacy — keeps checkpoint sync enabled but fully reconstructs the Sapling/Orchard trees per block. | | **Checkpoint sync disabled** | `consensus.checkpoint_sync = false` (any storage mode) | Legacy — fully reconstructs the Sapling/Orchard trees per block, using only mandatory checkpoints. | Gating fast on `checkpoint_sync` is also a correctness precondition: the embedded last checkpoint height @@ -198,17 +198,17 @@ Canopy mandatory checkpoint, so there is no valid last checkpoint height to resu `zebra-consensus`. Precedence is resolved by a pure, unit-tested `select_source_mode` (no process env, no embedded -files in the decision — `consensus.checkpoint_sync`, `consensus.disable_vct_fast_sync`, and the +files in the decision — `consensus.checkpoint_sync`, `consensus.vct_fast_sync`, and the embedded-frontier presence are passed in as plain inputs): -1. `consensus.checkpoint_sync = false`, `consensus.disable_vct_fast_sync = true`, or a network +1. `consensus.checkpoint_sync = false`, `consensus.vct_fast_sync = false`, or a network with **no embedded frontier** → **legacy** (no VCT state, zero overhead); 2. else → **peer** (the default under checkpoint sync where embedded frontiers exist). The earlier file-backed checkpoint/fixture root source (`VCT_FAST`/`VCT_FIXTURE`) and capture mode (`VCT_CAPTURE`) were transient integration scaffolding before peer delivery existed and have been removed. `VCT_REGTEST_FRONTIER` remains as a Regtest final-frontier test hook. -`consensus.disable_vct_fast_sync = true` is the supported user-facing way to force the legacy +`consensus.vct_fast_sync = false` is the supported user-facing way to force the legacy committer without disabling checkpoint sync (the deliberate opt-out for the default-on path; see the status note at the top of this document). @@ -427,16 +427,16 @@ occurred — the needed `H+1` witness is merely not buffered yet. to `pruning_metadata`, not a reuse — pruning drops tx bytes and keeps trees, fast-sync drops the per-height trees; a DB can be both. Because fast sync deletes nothing, a **completed** fast-synced DB (tip at/above the last checkpoint height) **reopens in any storage mode** — a reopen loses no servable data, -and `consensus.disable_vct_fast_sync = true` or `consensus.checkpoint_sync = false` simply resumes +and `consensus.vct_fast_sync = false` or `consensus.checkpoint_sync = false` simply resumes the legacy recompute from the real tip frontier. The one reopen that _is_ refused is an **interrupted** fast sync (frozen frontier, tip below the last checkpoint height) reopened with the fast path disabled (legacy mode — -`consensus.disable_vct_fast_sync = true`, `consensus.checkpoint_sync = false`, or no embedded +`consensus.vct_fast_sync = false`, `consensus.checkpoint_sync = false`, or no embedded frontier). The on-disk frontier is stale and no source can supply the verified roots, so the fail-closed policy (§8) would refuse every below-last checkpoint height block forever. The open guard refuses with a clear recovery path (finish the fast sync under `consensus.checkpoint_sync = true` and -`consensus.disable_vct_fast_sync = false`, or re-sync from genesis) instead of stalling silently. +`consensus.vct_fast_sync = true`, or re-sync from genesis) instead of stalling silently. Guards: per-height tree reads return `None` below the last checkpoint height (before the backward search, so no stale tree and no panic); `z_gettreestate` returns a typed archive-mode error below the last checkpoint height; genesis-root and subtree format-validity checks skip fast-synced DBs. @@ -623,7 +623,7 @@ Live commit-path counters distinguish the fast and legacy paths and the failure | Metric | Meaning | | --- | --- | | `state.vct.fast.block.count` | block folded supplied roots, skipped the recompute | -| `state.vct.legacy.block.count` | block recomputed the frontier (`consensus.disable_vct_fast_sync = true`, `consensus.checkpoint_sync = false`, or fell back outside the frozen window) | +| `state.vct.legacy.block.count` | block recomputed the frontier (`consensus.vct_fast_sync = false`, `consensus.checkpoint_sync = false`, or fell back outside the frozen window) | | `state.vct.prevalidated.block.count` | dedup sub-case: the previous fast block's look-ahead already validated this header | | `state.vct.root.rejected.count` | supplied root failed verification and was deleted for re-delivery | | `state.vct.root.unavailable.count` | frozen-frontier height with no valid root; commit refused (retryable) | @@ -644,7 +644,7 @@ asserts to prove roots actually came over the wire rather than a silent legacy s invalid-marker / unrequested-roots rejections (`decode_rejects_tree_aux_roots_when_not_requested`, `non_finalized_response_carrying_tree_aux_roots_is_malformed`) and the byte-budget clamp with - roots requested; `select_source_mode` precedence (`consensus.disable_vct_fast_sync = true` or + roots requested; `select_source_mode` precedence (`consensus.vct_fast_sync = false` or `consensus.checkpoint_sync = false` ⇒ legacy regardless of storage mode or embedded frontier; checkpoint sync + enabled VCT + embedded frontier ⇒ peer); a completed fast-synced DB reopens in archive mode (`reopening_fast_synced_database_in_archive_mode_succeeds`) while an interrupted diff --git a/docs/plans/headersync_roots_review.md b/docs/plans/headersync_roots_review.md index dd5e62366bd..be65a6af8db 100644 --- a/docs/plans/headersync_roots_review.md +++ b/docs/plans/headersync_roots_review.md @@ -33,7 +33,7 @@ mandatory-roots rule on _ranged_ requests does not starve the tip. 1. **`zebra-state` proptest `service::finalized_state::tests::prop::vct_frozen_frontier_survives_reopen`.** Panics at `finalized_state.rs:551`: "database was previously synced in verified commitment tree mode ... fast path ... is disabled. Set `consensus.checkpoint_sync = true` - and `consensus.disable_vct_fast_sync = false`...". This is #254's VCT fast-sync resume + and `consensus.vct_fast_sync = true`...". This is #254's VCT fast-sync resume gate; the proptest reopen config doesn't satisfy the resume preconditions. Verified to fail identically on the base branch. Relies on later VCT-resume wiring → flup. diff --git a/zebra-consensus/src/config.rs b/zebra-consensus/src/config.rs index 2a56842bbd2..07b291cc2ae 100644 --- a/zebra-consensus/src/config.rs +++ b/zebra-consensus/src/config.rs @@ -26,8 +26,8 @@ pub struct Config { /// /// Disabling this option makes Zebra start full validation earlier. /// It is slower and less secure. - /// To keep checkpoint sync enabled but force-disable the initial VCT fast-sync rollout, use - /// [`disable_vct_fast_sync`](Self::disable_vct_fast_sync) instead. + /// To keep checkpoint sync enabled but opt out of the initial VCT fast-sync rollout, set + /// [`vct_fast_sync`](Self::vct_fast_sync) to `false`. /// /// Zebra requires some checkpoints to simplify validation of legacy network upgrades. /// Required checkpoints are always active, even when this option is `false`. @@ -38,26 +38,26 @@ pub struct Config { /// release. pub checkpoint_sync: bool, - /// Force-disable the verified-commitment-trees fast sync path during its initial rollout. + /// Use the verified-commitment-trees fast sync path during its initial rollout. /// - /// This keeps [`checkpoint_sync`](Self::checkpoint_sync) enabled while forcing the legacy - /// per-block Sapling/Orchard tree recompute in both Archive and Pruned storage modes. Set to - /// `false` by default: checkpoint sync uses VCT fast sync on networks with embedded handoff - /// frontiers. - pub disable_vct_fast_sync: bool, + /// `true` by default: checkpoint sync folds in verified Sapling/Orchard roots and skips the + /// per-block tree recompute on networks with embedded handoff frontiers. Set to `false` to + /// keep [`checkpoint_sync`](Self::checkpoint_sync) enabled while forcing the legacy per-block + /// recompute in both Archive and Pruned storage modes. + pub vct_fast_sync: bool, } impl From for Config { fn from( InnerConfig { checkpoint_sync, - disable_vct_fast_sync, + vct_fast_sync, .. }: InnerConfig, ) -> Self { Self { checkpoint_sync, - disable_vct_fast_sync, + vct_fast_sync, } } } @@ -66,12 +66,12 @@ impl From for InnerConfig { fn from( Config { checkpoint_sync, - disable_vct_fast_sync, + vct_fast_sync, }: Config, ) -> Self { Self { checkpoint_sync, - disable_vct_fast_sync, + vct_fast_sync, _debug_skip_parameter_preload: false, } } @@ -88,7 +88,7 @@ pub struct InnerConfig { pub checkpoint_sync: bool, /// See [`Config`] for more details. - pub disable_vct_fast_sync: bool, + pub vct_fast_sync: bool, #[serde(skip_serializing, rename = "debug_skip_parameter_preload")] /// Unused config field for backwards compatibility. @@ -102,7 +102,7 @@ impl Default for Config { fn default() -> Self { Self { checkpoint_sync: true, - disable_vct_fast_sync: false, + vct_fast_sync: true, } } } @@ -111,7 +111,7 @@ impl Default for InnerConfig { fn default() -> Self { Self { checkpoint_sync: Config::default().checkpoint_sync, - disable_vct_fast_sync: Config::default().disable_vct_fast_sync, + vct_fast_sync: Config::default().vct_fast_sync, _debug_skip_parameter_preload: false, } } @@ -122,19 +122,19 @@ mod tests { use super::*; #[test] - fn disable_vct_fast_sync_defaults_false_and_converts_through_inner_config() { - assert!(!Config::default().disable_vct_fast_sync); + fn vct_fast_sync_defaults_true_and_converts_through_inner_config() { + assert!(Config::default().vct_fast_sync); let force_disabled = Config::from(InnerConfig { checkpoint_sync: true, - disable_vct_fast_sync: true, + vct_fast_sync: false, _debug_skip_parameter_preload: false, }); assert!(force_disabled.checkpoint_sync); - assert!(force_disabled.disable_vct_fast_sync); + assert!(!force_disabled.vct_fast_sync); let inner = InnerConfig::from(force_disabled); - assert!(inner.disable_vct_fast_sync); + assert!(!inner.vct_fast_sync); } } diff --git a/zebra-network/src/zakura/block_sync/config.rs b/zebra-network/src/zakura/block_sync/config.rs index a295a465523..dc9322b2b4a 100644 --- a/zebra-network/src/zakura/block_sync/config.rs +++ b/zebra-network/src/zakura/block_sync/config.rs @@ -76,8 +76,6 @@ pub const BS_CHECKPOINT_RANGE_BYTE_FLOOR: u64 = 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 central floor-watchdog cadence. -pub const DEFAULT_BS_FLOOR_WATCHDOG_TICK: Duration = Duration::from_secs(1); /// 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 reserved for later advertisement. @@ -176,9 +174,6 @@ pub struct ZakuraBlockSyncConfig { pub max_reorder_lookahead_bytes: u64, /// Maximum speculative body heights tracked above the download floor. pub max_reorder_lookahead_blocks: u32, - /// Cadence for the central floor watchdog that rescues expired floor claims. - #[serde(with = "humantime_serde")] - pub floor_watchdog_tick: Duration, /// How long to avoid reassigning an expired floor height to the same peer. #[serde(with = "humantime_serde")] pub floor_peer_avoid_cooldown: Duration, @@ -218,7 +213,6 @@ impl Default for ZakuraBlockSyncConfig { max_inflight_block_bytes: DEFAULT_BS_MAX_INFLIGHT_BLOCK_BYTES, max_reorder_lookahead_bytes: DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BYTES, max_reorder_lookahead_blocks: DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BLOCKS, - floor_watchdog_tick: DEFAULT_BS_FLOOR_WATCHDOG_TICK, floor_peer_avoid_cooldown: DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN, max_submitted_block_applies: DEFAULT_BS_MAX_SUBMITTED_BLOCK_APPLIES, request_timeout: DEFAULT_BS_REQUEST_TIMEOUT, @@ -258,13 +252,6 @@ impl ZakuraBlockSyncConfig { .min(self.max_inflight_block_bytes) } - /// Return the watchdog tick clamped to a positive duration no larger than the request timeout. - pub fn effective_floor_watchdog_tick(&self) -> Duration { - self.floor_watchdog_tick - .min(self.request_timeout) - .max(Duration::from_millis(1)) - } - /// 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)) diff --git a/zebra-network/src/zakura/block_sync/events.rs b/zebra-network/src/zakura/block_sync/events.rs index eff4ef56031..3178b5c1f13 100644 --- a/zebra-network/src/zakura/block_sync/events.rs +++ b/zebra-network/src/zakura/block_sync/events.rs @@ -169,8 +169,8 @@ pub enum BlockSyncMisbehavior { pub(super) enum RoutineToReactor { /// A routine received a `Status` and updated its own servable/caps + the /// registry. The reactor advertises our `Status` reply and republishes the - /// candidate set. `send_reply` is the routine's rate-meter decision (the - /// previous `unsolicited.try_take`) for whether a reply is due this time. + /// candidate set. `send_reply` is the routine's rate-meter decision for whether + /// a reply is due this time. StatusReceived { /// Peer whose status was applied. peer: ZakuraPeerId, diff --git a/zebra-network/src/zakura/block_sync/peer_registry.rs b/zebra-network/src/zakura/block_sync/peer_registry.rs index cc79e576fee..241779ea0e1 100644 --- a/zebra-network/src/zakura/block_sync/peer_registry.rs +++ b/zebra-network/src/zakura/block_sync/peer_registry.rs @@ -51,9 +51,8 @@ pub(super) struct Entry { /// reject-rollback window. pub(super) outstanding: BTreeMap, /// Routine-published slot diagnostics (trace only): the per-peer download - /// window state the reactor used to read off `PeerBlockState` for the periodic - /// `BLOCK_SYNC_STATE` row. Updated whenever the routine issues/finishes/times - /// out a request. + /// window state the reactor reads for the periodic `BLOCK_SYNC_STATE` row. + /// Updated whenever the routine issues/finishes/times out a request. pub(super) slots: SlotDiagnostics, /// Heights this peer may not re-take until the given instant. pub(super) retry_avoid: BTreeMap, @@ -426,6 +425,20 @@ impl PeerRegistry { (servable, outstanding) } + /// The soonest deadline among all peer claims for one height, if any. Lets the + /// reactor arm its floor watchdog to the exact expiry without allocating a + /// claim snapshot on every loop iteration. + pub(super) fn earliest_outstanding_deadline_at( + &self, + height: block::Height, + ) -> Option { + let peers = self.lock(); + peers + .values() + .filter_map(|entry| entry.outstanding.get(&height).map(|meta| meta.deadline)) + .min() + } + /// Snapshot all peer claims for one height. pub(super) fn outstanding_claims_at(&self, height: block::Height) -> Vec { let peers = self.lock(); @@ -502,7 +515,7 @@ pub(super) struct DirectionStatusCounts { } /// Hard outbound concurrency ceiling for a peer with the given advertised -/// in-flight cap (the routine's slot bound; mirrors `PeerBlockState`). +/// in-flight cap (the routine's slot bound). pub(super) fn hard_outbound_capacity(max_inflight_requests: u32) -> usize { usize::try_from(max_inflight_requests) .expect("u32 max inflight requests fits in usize on supported targets") diff --git a/zebra-network/src/zakura/block_sync/peer_routine.rs b/zebra-network/src/zakura/block_sync/peer_routine.rs index f4ec415eec7..195e8da4754 100644 --- a/zebra-network/src/zakura/block_sync/peer_routine.rs +++ b/zebra-network/src/zakura/block_sync/peer_routine.rs @@ -16,13 +16,10 @@ //! on the byte budget + per-peer slots: `take_in_range(servable_low, //! servable_high, n)` uses `servable_high` as the upper bound. //! -//! The download logic is **moved, not rewritten** from the previous reactor: the -//! want-work fill loop ports `fill_peer`, the matched-body tail ports -//! `handle_block`, and the unmatched fallthroughs port `accept_unmatched_queued_body` -//! / the `ignore_*` helpers / `stale_adjusted_outstanding_disposition` verbatim, -//! changing only where the state lives (now routine-local or the -//! [`PeerRegistry`]) and that inbound now arrives as a decoded frame from this -//! task's own `FramedRecv` rather than a `PeerInput` channel. +//! All per-peer download state lives routine-local or in the shared +//! [`PeerRegistry`], and inbound traffic arrives as decoded frames from this +//! task's own `FramedRecv`: a want-work fill loop, the matched-body tail, and the +//! unmatched-body fallthroughs all run in this one task. use std::collections::BTreeMap; @@ -60,11 +57,10 @@ use zebra_chain::{block, serialization::ZcashSerialize}; /// (RangeUnavailable / timeout / send-failure / disconnect-retry) before it will /// contest that height again. The window only has to be long enough that, on the /// single-threaded test runtime, the other routines woken by the same failure -/// `return_items` get a chance to take the contested work first — it is the -/// peer-local retry bias the central `fill_rotation_cursor` used to provide -///. It is -/// negligible against real sync timescales, and the height stays `pending` and -/// fully contestable by every other peer throughout. +/// `return_items` get a chance to take the contested work first — a peer-local +/// bias away from work this routine just failed. It is negligible against real +/// sync timescales, and the height stays `pending` and fully contestable by every +/// other peer throughout. const RETRY_AVOID_BACKOFF: Duration = Duration::from_millis(50); /// Poll interval while this peer's outbound stream queue is full. const OUTBOUND_FULL_POLL_INTERVAL: Duration = Duration::from_millis(10); @@ -81,8 +77,7 @@ fn release_counter_bytes(counter: &std::sync::atomic::AtomicU64, bytes: u64) { ); } -/// Outcome classification for finishing an outstanding request (ported verbatim -/// from the reactor's `OutstandingRangeDisposition`). +/// Outcome classification for finishing an outstanding request. #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum Disposition { Satisfied, @@ -119,13 +114,13 @@ pub(super) struct PeerRoutine { /// registry for the reactor's serving-side reads). max_blocks_per_response: u32, max_response_bytes: u32, - /// Rate meter for sending our `Status` reply to this peer's inbound `Status` - /// (the previous `unsolicited` meter; the reply decision is routine-local now, - /// the actual send stays reactor-side via `RoutineToReactor::StatusReceived`). + /// Rate meter for sending our `Status` reply to this peer's inbound `Status`. + /// The reply decision is routine-local; the actual send stays reactor-side via + /// `RoutineToReactor::StatusReceived`. status_reply_meter: super::state::RateMeter, - /// Rate meter gating how often this peer's `Status` frames are applied at all - /// (the previous `inbound_status` meter), so a status flood cannot spin the - /// routine. A status that grows the servable range bypasses the meter. + /// Rate meter gating how often this peer's `Status` frames are applied at all, + /// so a status flood cannot spin the routine. A status that grows the servable + /// range bypasses the meter. inbound_status_meter: super::state::RateMeter, /// Heights this routine recently returned on a failure, mapped to the instant /// after which it may re-take them. While avoided, the routine leaves the @@ -195,8 +190,7 @@ impl PeerRoutine { ); // Defer the first Status reply: the reactor already sends a connect-status // on `PeerConnected`, so the peer's first inbound `Status` should not also - // trigger an immediate reply (matches the previous `peer_connected` - // `unsolicited.mark_taken`). + // trigger an immediate reply. let mut status_reply_meter = status_reply_meter; status_reply_meter.mark_taken(Instant::now()); let max_blocks_per_response = config.advertised_max_blocks_per_response(); @@ -234,7 +228,7 @@ impl PeerRoutine { /// Run the pipe-routine until stream close, cancellation, or a protocol /// reject. A reject returns `Err(SinkReject::protocol(..))` so the supervised - /// pipe tears the whole connection down, matching the previous `run_peer`. + /// pipe tears the whole connection down. pub(super) async fn run(mut self) -> Result<(), SinkReject> { // Local clones so the `Notified` futures below borrow these handles, not // `self` — `self.try_fill()` needs `&mut self` while the notifications are @@ -243,7 +237,7 @@ impl PeerRoutine { // `self.work`. let budget = self.budget.clone(); let work = self.work.clone(); - // The per-connection oversize guard the previous pipe applied at ingress. + // The per-connection oversize guard applied to inbound frames at ingress. let mut guard = block_sync_guard(); loop { // missed-wake safety: register both `Notify`s via @@ -276,9 +270,8 @@ impl PeerRoutine { match frame { // Decode the frame and run the download/serving dispatch // in this same task. A protocol reject propagates out so - // the supervised pipe cancels the connection (matches the - // previous `run_peer` reject path); the `Drop` guard returns - // unreceived work on the way out. + // the supervised pipe cancels the connection; the `Drop` + // guard returns unreceived work on the way out. Some(frame) => self.handle_frame(&mut guard, frame).await?, // Stream closed (peer gone): exit cleanly. `Drop` returns // unreceived outstanding heights and releases their budget. @@ -342,9 +335,8 @@ impl PeerRoutine { Ok(decoded) => decoded, Err(error) => { // A malformed frame is `MalformedMessage` misbehavior AND a fatal - // protocol reject for the whole connection (matches the previous - // `run_peer` decode-error path). Report via the shared channel, - // then reject; the report is best-effort and never blocks. + // protocol reject for the whole connection. Report via the shared + // channel, then reject; the report is best-effort and never blocks. let protocol_error = std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string()); tracing::debug!(peer = ?self.peer, ?error, "malformed Zakura block-sync frame"); @@ -410,9 +402,8 @@ impl PeerRoutine { /// Apply this peer's `Status` locally (servable range, caps, `received_status`) /// and into the registry, then ping the reactor to advertise our reply and - /// republish the candidate. Ports the reactor's previous `handle_status` - /// validate / rate-meter / upsert; the servable read for want-work is now this - /// routine's own fields. + /// republish the candidate. Runs the validate / rate-meter / upsert; the + /// servable read for want-work is this routine's own fields. fn handle_status(&mut self, status: BlockSyncStatus) { if status.servable_low > status.servable_high { let _ = self @@ -520,29 +511,26 @@ impl PeerRoutine { // ===================== want-work fill loop (ports `fill_peer`) =========== /// Fill this peer's available slots in a single pass, letting the byte budget - /// (re-checked each iteration via `try_reserve`) be the congestion window. - /// Ported from the reactor's `fill_peer`; the only changes are where the - /// per-peer state lives (now routine-local / the registry) and the handles. + /// (re-checked each iteration via `try_reserve`) be the congestion window. The + /// per-peer state is routine-local / in the registry. /// /// There is no floor gate: downloads are governed by the byte budget and /// per-peer slots, never floor-distance / near-tip lag. async fn try_fill(&mut self) { // Reconcile the adaptive window's hard cap with the peer's currently // advertised `max_inflight_requests` (it may have grown/shrunk via a - // `Status`; `handle_status` set `window.max_inflight_requests`). Mirrors - // the previous `handle_status` clamp of the window / recovery slots to the - // new hard capacity. + // `Status`; `handle_status` set `window.max_inflight_requests`), clamping + // the window / recovery slots to the new hard capacity. let hard = self.window.hard_outbound_capacity(); self.window.outbound_request_window = self.window.outbound_request_window.min(hard).max(1); self.window.timeout_recovery_slots = self.window.timeout_recovery_slots.min(hard); // GC this routine's own fully-covered outstanding requests: when the // download floor passes the end of a request, its bodies are no longer // needed, so release its reservation and free its slot promptly rather - // than waiting for the request's own timeout. This is the floor used for - // GC of *our own* covered requests, never a fetch - // throttle — it replaces the previous reactor `drop_outstanding_through` - // without the cross-peer churn the spec warned about (a partially-received - // request whose suffix is still above the floor is left in place). + // than waiting for the request's own timeout. This GCs *our own* covered + // requests; it is never a fetch throttle and never churns other peers (a + // partially-received request whose suffix is still above the floor is left + // in place). self.gc_committed_outstanding(); // Drop expired retry-avoid entries: those heights are contestable by this // routine again. @@ -830,8 +818,7 @@ impl PeerRoutine { } } - /// Refill low-water mark in blocks (ported from the reactor's - /// `refill_low_water_blocks`, but for a single peer's caps). + /// Refill low-water mark in blocks, computed from a single peer's caps. fn refill_low_water_blocks(&self) -> usize { let max_blocks_per_response = usize::try_from(self.config.advertised_max_blocks_per_response()).unwrap_or(usize::MAX); @@ -1143,11 +1130,10 @@ impl PeerRoutine { self.trace_body_sequencer_sent(height, sequencer_send_started.elapsed(), ok); } - /// Ported from the reactor's `accept_unmatched_queued_body`, minus the - /// disconnected-peer branch (a routine only runs for a live peer): a queued - /// height served by a peer that lost its original requester returns to the - /// queue. The routine reserves the body's actual size, claims the height into - /// `in_flight`, and forwards it. + /// Accept a queued body whose original requester is gone (a routine only runs + /// for a live peer, so there is no disconnected-peer branch): the routine + /// reserves the body's actual size, claims the height into `in_flight`, and + /// forwards it. async fn accept_unmatched_queued_body( &mut self, height: block::Height, @@ -1291,8 +1277,7 @@ impl PeerRoutine { // We reach this only when *this* peer has no outstanding request starting // at `start_height`; the registry answers whether another peer is actively // requesting a range covering it (cross-peer fanout/retry race), in which - // case the terminator is dropped quietly rather than scored. A faithful - // (slightly wider) superset of the previous start-keyed check. + // case the terminator is dropped quietly rather than scored. if !self.registry.has_outstanding_height(start_height) { return false; } @@ -1313,11 +1298,11 @@ impl PeerRoutine { /// raced ahead of the producer's asynchronous `work.extend`. The peer asked /// for and served this range honestly, so scoring it the *hard* /// `UnsolicitedBlock`/`UnsolicitedDone` (immediate, thresholdless disconnect) - /// would churn honest peers on every reorg. The previous serial reactor - /// avoided this by dropping outstanding in the same loop turn as the reset; - /// the Sequencer-task split (Sequencer task) made that drop asynchronous, opening this - /// window — restore the no-churn property by dropping the response quietly. A - /// response *outside* the peer's advertised range is still scored. + /// would churn honest peers on every reorg. The reset that drops outstanding + /// runs on the Sequencer task asynchronously, so an honest in-flight response + /// can arrive after its `outstanding` entry is gone — drop it quietly to keep + /// the no-churn property. A response *outside* the peer's advertised range is + /// still scored. fn ignore_servable_range_response(&self, height: block::Height, response_kind: &str) -> bool { if !self.received_status || height <= self.download_floor() || height > self.servable_high { return false; @@ -1380,9 +1365,8 @@ impl PeerRoutine { self.finish_outstanding_at(index, disposition); } - /// Ported from the reactor's `stale_adjusted_outstanding_disposition`: a late - /// response can still match after the floor moved through its prefix; mark - /// the stale prefix satisfied and retry only the remaining suffix. + /// A late response can still match after the floor moved through its prefix; + /// mark the stale prefix satisfied and retry only the remaining suffix. fn stale_adjusted_disposition(&mut self, index: usize, current: Disposition) -> Disposition { let tip = self.download_floor(); let Some(outstanding) = self.window.outstanding.get_mut(index) else { @@ -1455,7 +1439,7 @@ impl PeerRoutine { /// Publish this peer's current *unreceived* in-flight height metadata to the /// registry, so the producer's `!has_outstanding_request` filter and the /// low-water `total_unreceived` gate read the same per-request-granularity - /// count the previous reactor used (`expected_blocks.len() − received.len()`). + /// count (`expected_blocks.len() − received.len()`). /// Received-but-uncommitted heights are excluded here because they are held in /// `work.in_flight` instead — the producer's `!in_flight_contains` clause /// already keeps them out of `pending`. @@ -1553,9 +1537,9 @@ impl PeerRoutine { }); } - /// Trace a decoded inbound message (the previous reactor's `trace_message_received`, - /// now emitted in the routine that decoded it). Records the message kind only; - /// the per-variant field detail lives on the reactor's heavier trace path. + /// Trace a decoded inbound message in the routine that decoded it. Records the + /// message kind only; the per-variant field detail lives on the reactor's + /// heavier trace path. fn trace_message_received(&self, msg: &BlockSyncMessage) { self.emit(bs_trace::BLOCK_MESSAGE_RECEIVED, |row| { row.insert( diff --git a/zebra-network/src/zakura/block_sync/reactor.rs b/zebra-network/src/zakura/block_sync/reactor.rs index 9281674571b..61b8494ed11 100644 --- a/zebra-network/src/zakura/block_sync/reactor.rs +++ b/zebra-network/src/zakura/block_sync/reactor.rs @@ -256,14 +256,19 @@ impl BlockSyncReactor { .status_refresh_interval .max(Duration::from_millis(1)), ); - let mut floor_watchdog_ticks = - time::interval(self.startup.config.effective_floor_watchdog_tick()); self.query_needed_blocks().await; self.publish_metrics(); self.refresh_throughput(); self.trace_sync_state(); loop { + // Arm the floor watchdog to the earliest outstanding floor-claim + // deadline (event-driven, like the per-peer routine's own-timeout + // arm): force-cancel an expired floor request exactly when it expires + // instead of polling. Rebuilt each iteration, so any event that adds, + // completes, or advances the floor re-arms it to the next deadline. + let floor_watchdog = self.earliest_floor_deadline_sleep(); + tokio::pin!(floor_watchdog); tokio::select! { _ = self.startup.shutdown.cancelled() => break, event = self.lifecycle.recv() => { @@ -345,7 +350,7 @@ impl BlockSyncReactor { self.trace_sync_state(); } _ = status_ticks.tick() => self.flush_status_refresh().await, - _ = floor_watchdog_ticks.tick() => { + _ = &mut floor_watchdog => { self.run_floor_watchdog(Instant::now()); self.publish_metrics(); } @@ -353,6 +358,19 @@ impl BlockSyncReactor { } } + /// Sleep future resolving at the earliest outstanding floor-claim deadline, so + /// the reactor force-cancels an expired floor request exactly when it expires + /// rather than on a fixed poll. Defaults to a long idle sleep when no floor + /// claim is outstanding; any reactor event recomputes it on the next iteration. + fn earliest_floor_deadline_sleep(&self) -> time::Sleep { + let earliest = next_height(self.request_floor) + .and_then(|height| self.registry.earliest_outstanding_deadline_at(height)); + match earliest { + Some(deadline) => time::sleep(deadline.saturating_duration_since(Instant::now())), + None => time::sleep(Duration::from_secs(3600)), + } + } + fn run_floor_watchdog(&mut self, now: Instant) { let Some(height) = next_height(self.request_floor) else { return; @@ -537,8 +555,7 @@ impl BlockSyncReactor { let mut peer_state = PeerBlockState::new(session, &self.startup.config); // Consume the status-advertisement refresh allowance: the connect Status // below counts as this peer's first advertisement, so the next periodic - // refresh must wait a full interval before re-sending (matches the previous - // `unsolicited.mark_taken` at connect). + // refresh must wait a full interval before re-sending. peer_state.refresh_meter.mark_taken(Instant::now()); self.state.peers.insert(peer.clone(), peer_state); @@ -744,8 +761,7 @@ impl BlockSyncReactor { } /// React to the latest progress view from the Sequencer task: update the - /// reactor's mirrors, then run the serving/peer/candidate/producer - /// half that used to follow the inline Sequencer mutation + /// reactor's mirrors, then run the serving/peer/candidate/producer half /// (status refresh, candidate prune, drop-outstanding, re-query, re-schedule). async fn on_sequencer_view_changed(&mut self, view: SequencerView) { // Always update the mirrors so the producer lower bound, @@ -766,11 +782,11 @@ impl BlockSyncReactor { self.state.servable_hash = view.verified_hash; // The heavy serving/peer/candidate/producer reaction (drop-outstanding, - // prune, status, query, schedule) ran in the single-task version for a - // frontier advance, reset, or apply-finished — never for a pure body - // buffer/submit, which only reschedules the forwarding peer (the reactor - // already did that after forwarding `AcceptBody`). The `reaction_epoch` - // advances exactly for those inputs. + // prune, status, query, schedule) runs only for a frontier advance, reset, + // or apply-finished — never for a pure body buffer/submit, which only + // reschedules the forwarding peer (the reactor already did that after + // forwarding `AcceptBody`). The `reaction_epoch` advances exactly for those + // inputs. if !reaction_advanced { return; } @@ -1170,11 +1186,9 @@ impl BlockSyncReactor { } fn local_body_work_blocks(&self) -> usize { - // The unreceived in-flight heights now live in the routines, mirrored into - // the registry's per-peer outstanding set (per-request granularity: each - // entry is one still-unreceived requested height). `total_unreceived` sums - // them — the same count the old per-peer `expected_blocks − received` - // produced. + // The unreceived in-flight heights live in the routines, mirrored into the + // registry's per-peer outstanding set (per-request granularity: each entry + // is one still-unreceived requested height). `total_unreceived` sums them. let outstanding = self.registry.total_unreceived(); // Count only the download pipeline (pending WorkQueue heights + the diff --git a/zebra-network/src/zakura/block_sync/sequencer.rs b/zebra-network/src/zakura/block_sync/sequencer.rs index 068ca07781a..cf2f1e22ada 100644 --- a/zebra-network/src/zakura/block_sync/sequencer.rs +++ b/zebra-network/src/zakura/block_sync/sequencer.rs @@ -12,8 +12,8 @@ //! re-query, attribute misbehavior) is expressed as a value the reactor acts //! on, not performed here. //! -//! The logic is preserved verbatim from the reactor; only its boundary changes. -//! That boundary is what lets a later stage move the Sequencer onto its own task. +//! Keeping the Sequencer free of download-side state is what lets it run on its +//! own serial task ([`super::sequencer_task`]), off the reactor's thread. use super::{events::BlockApplyToken, reorder::*, state::*, *}; diff --git a/zebra-network/src/zakura/block_sync/sequencer_task.rs b/zebra-network/src/zakura/block_sync/sequencer_task.rs index 473a6655f59..6c1963706ef 100644 --- a/zebra-network/src/zakura/block_sync/sequencer_task.rs +++ b/zebra-network/src/zakura/block_sync/sequencer_task.rs @@ -8,11 +8,12 @@ //! events over a non-blocking control channel. The reactor learns committed //! progress back over a non-blocking `watch` ([`SequencerView`]). //! -//! The logic in each input handler is the **verbatim** logic that used to run -//! inline in the matching reactor handler (`handle_block`'s body-acceptance tail, -//! `apply_state_frontiers_changed`'s Sequencer half, `handle_chain_tip_reset`, -//! `handle_block_apply_finished`); only its location and the budget/work/actions -//! handles it uses move here. See the "Sequencer task". +//! Each input handler owns one stage of the commit pipeline: the body-acceptance +//! tail (`handle_accept_body`), the verified-tip frontier advance +//! (`handle_frontier_advance`), the chain-tip reset (`handle_frontier_reset`), and +//! the apply completion (`handle_apply_finished`). They mutate the `Sequencer`, +//! byte budget, and work queue directly and emit `SubmitBlock`/`Misbehavior` +//! actions on the same channel the reactor uses. use super::{ events::*, @@ -24,12 +25,6 @@ use super::{ *, }; -/// How often the Sequencer task checks whether the byte budget is starving the -/// commit-unblocking (lowest pending) height and sheds the speculative top of the -/// reorder buffer to fund it. Bounds the recovery latency when no bodies are -/// flowing to trigger the inline check (e.g. once outstanding requests drain). -const FLOOR_STARVATION_SHED_INTERVAL: Duration = Duration::from_millis(500); - /// Favor the lowest needed height over the speculative high tail. /// /// While the byte budget cannot fund even one worst-case request yet the lowest @@ -39,10 +34,10 @@ const FLOOR_STARVATION_SHED_INTERVAL: Duration = Duration::from_millis(500); /// invariant) for later re-fetch. Because another top can always be shed, a low /// retry never blocks on budget; the floor can never wedge behind a full buffer, /// and under a stall the speculative tail is shed and the chain fills bottom-up, -/// which also bounds the reorder backlog. Floor requesters also call this -/// synchronously through [`SequencerControlInput::FundFloorReservation`], so the -/// rescue path is demand-driven with a periodic backstop. Returns whether it shed -/// anything. +/// which also bounds the reorder backlog. The rescue path is purely demand-driven: +/// it runs inline after each accepted body and synchronously when a floor requester +/// needs budget through [`SequencerControlInput::FundFloorReservation`]. Returns +/// whether it shed anything. pub(super) fn shed_top_until_available( budget: &mut ByteBudget, work: &WorkQueue, @@ -165,9 +160,9 @@ pub(super) struct SequencerView { pub(super) reset_epoch: u64, /// Increments once per processed frontier/reset/apply input (NOT per accepted /// body). The reactor runs its heavy serving/producer/schedule reaction only - /// when this advances, mirroring the single-task version where a pure body - /// buffer/submit reran nothing but the forwarding peer's reschedule, while a - /// frontier advance, reset, or apply-finished always reran query/schedule. + /// when this advances: a pure body buffer/submit needs nothing but the + /// forwarding peer's own reschedule, while a frontier advance, reset, or + /// apply-finished must re-query and reschedule. pub(super) reaction_epoch: u64, pub(super) reorder_len: u64, pub(super) applying_len: u64, @@ -261,13 +256,9 @@ impl SequencerTask { } pub(super) async fn run(mut self) { - // Periodic shed backstop: catches budget starvation of the floor even when - // no bodies/control events are arriving to trigger the inline checks. - let mut shed_tick = tokio::time::interval(FLOOR_STARVATION_SHED_INTERVAL); - shed_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); - // Track input closure explicitly: the always-ready shed timer means the - // `select!` never falls through to an `else`, so shut down only once both - // input channels have closed. + // Track input closure explicitly so the loop exits once both inputs close: + // a `select!` whose arms are all disabled with no `else` panics, so the + // top-of-loop guard breaks out before the last open channel is gated off. let mut control_open = true; let mut body_open = true; loop { @@ -305,26 +296,15 @@ impl SequencerTask { None => body_open = false, } } - - _ = shed_tick.tick() => { - if shed_top_for_floor_starvation( - &mut self.budget, - &self.work, - &mut self.sequencer, - ) { - self.publish_view(); - } - } } } } async fn handle_control_input(&mut self, input: SequencerControlInput) -> bool { - // Each handler reports whether it did work that the single-task version - // would have followed with the reactor's heavy serving/producer/schedule - // tail. Bumping `reaction_epoch` only then keeps the reactor from - // re-querying/-scheduling on a pure body buffer/submit or a no-op - // (stale/duplicate) apply completion. + // Each handler reports whether it did work that needs the reactor's heavy + // serving/producer/schedule tail. Bumping `reaction_epoch` only then keeps + // the reactor from re-querying/-scheduling on a pure body buffer/submit or + // a no-op (stale/duplicate) apply completion. match input { SequencerControlInput::FrontierAdvance { frontiers, @@ -383,9 +363,9 @@ impl SequencerTask { ); } - /// Body-acceptance tail (verbatim from `handle_block` ~885-907 and - /// `accept_unmatched_queued_body` ~1170-1183): offer the body, release on - /// `Redundant`, then drain ready prefix into applying and submit. + /// Body-acceptance tail: offer the body to the reorder buffer, release its + /// bytes on a `Redundant` outcome, then drain the ready contiguous prefix into + /// applying and submit it. async fn handle_accept_body(&mut self, body: SequencedBody) { let queued_elapsed = body.received_at.elapsed(); let outcome = match self.sequencer.accept_buffered_body( @@ -405,20 +385,19 @@ impl SequencerTask { self.release_contiguous_blocks().await; } - /// Sequencer half of `apply_state_frontiers_changed` (verbatim from - /// reactor.rs ~447-478, including the stale guard). + /// Apply a verified-tip frontier advance: fold finalized height forward, drop + /// stale updates, then advance the verified tip and floor and drain the newly + /// contiguous prefix. async fn handle_frontier_advance( &mut self, frontiers: BlockSyncFrontiers, release_applied: bool, ) { - // Fold the finalized height forward unconditionally (matches the original's - // first line), then drop a stale update. The verified tip is monotonic: an - // advance whose target is below our verified tip must be a no-op, never a - // regression. This guard is the original `apply_state_frontiers_changed`'s - // `verified_block_tip < verified_tip() => return None`; without it the - // second growth-reset path (`< floor`, which permits `< verified_tip`) would - // call `advance_verified_tip` with a lower tip and regress it. + // Fold the finalized height forward unconditionally, then drop a stale + // update. The verified tip is monotonic: an advance whose target is below + // our verified tip must be a no-op, never a regression. Without this guard + // the second growth-reset path (`< floor`, which permits `< verified_tip`) + // would call `advance_verified_tip` with a lower tip and regress it. self.finalized_height = self.finalized_height.max(frontiers.finalized_height); if frontiers.verified_block_tip < self.sequencer.verified_tip() { return; @@ -435,9 +414,10 @@ impl SequencerTask { } } - /// The Sequencer/work/budget body of `handle_chain_tip_reset` (verbatim from - /// reactor.rs 502-576). The peer-outstanding reads are replaced by the - /// precomputed `peer_*` bools. + /// Handle a chain-tip reset: classify it as growth (treat as an advance) or a + /// destructive reorg (pin tip/floor to the target, clear successor buffers, and + /// bump the reset epoch). The peer-outstanding clauses of the decision arrive as + /// the precomputed `peer_*` bools, since the reactor owns peer state. async fn handle_frontier_reset( &mut self, frontiers: BlockSyncFrontiers, @@ -465,8 +445,8 @@ impl SequencerTask { )) && reset_tip_matches_local_work { - // Growth-classified reset: treat as a frontier advance (same as the - // reactor's `handle_state_frontiers_changed` path), `release_applied`. + // Growth-classified reset: treat it as a frontier advance, releasing + // applied bodies. self.handle_frontier_advance(frontiers, true).await; return; } @@ -515,10 +495,11 @@ impl SequencerTask { self.reset_epoch = self.reset_epoch.saturating_add(1); } - /// Verbatim from `handle_block_apply_finished` (1443-1540), minus the - /// reactor-side serving/query/schedule/status tail (which the view reaction - /// runs). The embedded `local_frontier` advance is folded in as a frontier - /// advance with `release_applied: false`. + /// Handle a verifier apply completion: release the body's bytes and verifier + /// slot, fold in any embedded `local_frontier` as a frontier advance with + /// `release_applied: false`, and on a rejection roll the floor back below the + /// bad block so its range is re-requestable. Returns whether the reactor needs + /// its serving/query/schedule reaction (the view reaction runs that tail). async fn handle_apply_finished( &mut self, token: BlockApplyToken, @@ -528,8 +509,8 @@ impl SequencerTask { local_frontier: Option, ) -> bool { // A stale completion (no live applying entry, or token/hash mismatch) - // only decrements the submitted-apply record and returns; the single-task - // version ran no query/schedule tail here, so it needs no reaction. + // only decrements the submitted-apply record and returns; there is no + // query/schedule tail here, so it needs no reaction. let Some((applying_token, applying_hash)) = self.sequencer.applying_token_hash(height) else { self.sequencer.decrement_submitted_apply(height, hash); @@ -542,9 +523,8 @@ impl SequencerTask { let accepted_local_frontier = if let Some(frontiers) = local_frontier { // Fold the `local_frontier` advance in as a frontier advance without - // releasing committed applying bodies (`release_applied: false`), - // matching the inline `apply_state_frontiers_changed(.., false)` call. - // It is accepted only when it is not a stale (older-tip) update. + // releasing committed applying bodies (`release_applied: false`). It is + // accepted only when it is not a stale (older-tip) update. if frontiers.verified_block_tip < self.sequencer.verified_tip() { None } else { @@ -556,9 +536,9 @@ impl SequencerTask { }; if matches!(result, BlockApplyResult::Duplicate) && self.sequencer.verified_tip() < height { - // Stale duplicate for a height we have not verified to: the single-task - // version ran the serving/query tail only when the accepted local - // frontier advanced serving (an `old_serving_tip` existed). + // Stale duplicate for a height we have not verified to: the reactor + // needs the serving/query tail only when the accepted local frontier + // actually advanced serving. return accepted_local_frontier.is_some(); } let applying = self @@ -614,8 +594,7 @@ impl SequencerTask { true } - /// Drain the contiguous reorder prefix into applying and submit (verbatim - /// from `release_contiguous_blocks` + `submit_pending_blocks`). + /// Drain the contiguous reorder prefix into applying, then submit it. async fn release_contiguous_blocks(&mut self) { let _ = self.sequencer.drain_ready_into_applying(); self.submit_pending_blocks().await; diff --git a/zebra-network/src/zakura/block_sync/state.rs b/zebra-network/src/zakura/block_sync/state.rs index ba33e1931f7..bcd0e52b0f1 100644 --- a/zebra-network/src/zakura/block_sync/state.rs +++ b/zebra-network/src/zakura/block_sync/state.rs @@ -320,9 +320,9 @@ impl BlockSyncState { /// Adaptive per-peer outbound request window + outstanding requests. /// -/// Carved out of the old `PeerBlockState` so the window math stays unit-testable -/// while the per-peer download state moves into the spawned -/// [`PeerRoutine`](super::peer_routine) (per-peer routines). The routine embeds one of these. +/// Kept as a standalone type so the window math stays unit-testable; the per-peer +/// download state lives in the spawned [`PeerRoutine`](super::peer_routine), which +/// embeds one of these. #[derive(Clone, Debug)] pub(super) struct DownloadWindow { pub(super) max_inflight_requests: u32, @@ -513,9 +513,9 @@ pub(super) struct PeerBlockState { pub(super) direction: ServicePeerDirection, /// Per-peer rate meter for the reactor's `Status` *advertisement* refresh /// (serving-tip change broadcast + retry to peers that have not acknowledged - /// our Status). The previous `unsolicited` meter was dual-use; its inbound-status - /// *reply* half moved to the routine's `status_reply_meter`. This half stays - /// reactor-side because the reactor owns serving-tip advertisement. + /// our Status). The inbound-status *reply* half lives on the routine's + /// `status_reply_meter`; this half stays reactor-side because the reactor owns + /// serving-tip advertisement. pub(super) refresh_meter: RateMeter, pub(super) served_blocks_inflight: u32, pub(super) served_block_requests: VecDeque<(block::Height, Instant)>, diff --git a/zebra-network/src/zakura/block_sync/tests.rs b/zebra-network/src/zakura/block_sync/tests.rs index 13fb6fbeefa..a62b684022d 100644 --- a/zebra-network/src/zakura/block_sync/tests.rs +++ b/zebra-network/src/zakura/block_sync/tests.rs @@ -6,11 +6,10 @@ use super::*; use super::{ config::{ BS_CHECKPOINT_RANGE_BYTE_FLOOR, BS_PER_BLOCK_WORST_CASE_BYTES, DEFAULT_BS_FANOUT, - DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN, DEFAULT_BS_FLOOR_WATCHDOG_TICK, - DEFAULT_BS_MAX_INFLIGHT_BLOCK_BYTES, DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BLOCKS, - DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BYTES, DEFAULT_BS_MAX_RESPONSE_BYTES, - DEFAULT_BS_MAX_SUBMITTED_BLOCK_APPLIES, DEFAULT_BS_REQUEST_TIMEOUT, - MAX_BS_INFLIGHT_REQUESTS, MAX_BS_RESPONSE_BYTES, + DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN, DEFAULT_BS_MAX_INFLIGHT_BLOCK_BYTES, + DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BLOCKS, DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BYTES, + DEFAULT_BS_MAX_RESPONSE_BYTES, DEFAULT_BS_MAX_SUBMITTED_BLOCK_APPLIES, + DEFAULT_BS_REQUEST_TIMEOUT, MAX_BS_INFLIGHT_REQUESTS, MAX_BS_RESPONSE_BYTES, }, reactor::node_id_from_block_peer_id, reorder::*, @@ -707,7 +706,6 @@ fn block_sync_config_defaults_and_round_trips() { default.max_reorder_lookahead_blocks, DEFAULT_BS_MAX_REORDER_LOOKAHEAD_BLOCKS ); - assert_eq!(default.floor_watchdog_tick, DEFAULT_BS_FLOOR_WATCHDOG_TICK); assert_eq!( default.floor_peer_avoid_cooldown, DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN @@ -720,10 +718,6 @@ fn block_sync_config_defaults_and_round_trips() { default.floor_request_byte_reservation(), u64::from(DEFAULT_BS_MAX_RESPONSE_BYTES) ); - assert_eq!( - default.effective_floor_watchdog_tick(), - DEFAULT_BS_FLOOR_WATCHDOG_TICK - ); assert_eq!( default.effective_floor_peer_avoid_cooldown(), DEFAULT_BS_FLOOR_PEER_AVOID_COOLDOWN diff --git a/zebra-network/src/zakura/block_sync/work_queue.rs b/zebra-network/src/zakura/block_sync/work_queue.rs index dca36458296..e4058d1f824 100644 --- a/zebra-network/src/zakura/block_sync/work_queue.rs +++ b/zebra-network/src/zakura/block_sync/work_queue.rs @@ -200,6 +200,15 @@ impl WorkQueue { max_count: usize, max_estimated_bytes: u64, ) -> Vec<(block::Height, WorkItem)> { + // An empty count or inverted range is a caller bug, not a real "nothing to + // take": every caller computes `low <= high` and a positive count before + // calling. Assert it in debug/test builds; still return empty in release so + // a miscomputation degrades to a no-op rather than panicking a live node. + debug_assert!( + max_count > 0 && low <= high, + "take_in_range_budgeted requires a positive count and low <= high, \ + got max_count={max_count}, low={low:?}, high={high:?}" + ); if max_count == 0 || low > high { return Vec::new(); } @@ -496,8 +505,8 @@ impl WorkQueue { .fold(0u64, u64::saturating_add) } - /// Number of contiguous runs across `pending` (the old `queue_len` meaning: - /// one queued range per maximal contiguous run of heights). + /// Number of contiguous runs across `pending` (one queued range per maximal + /// contiguous run of heights). pub(super) fn pending_run_count(&self) -> usize { let inner = self.lock(); let mut runs = 0usize; @@ -551,7 +560,7 @@ impl WorkQueue { } /// Expected hash for a height in `pending` or `in_flight` (late-response - /// recovery; replaces the old `queued_hash_for_height`). + /// recovery). pub(super) fn hash_for_height(&self, height: block::Height) -> Option { let inner = self.lock(); inner diff --git a/zebra-state/src/config.rs b/zebra-state/src/config.rs index 3f73efd70df..9cf5a4a0a80 100644 --- a/zebra-state/src/config.rs +++ b/zebra-state/src/config.rs @@ -116,7 +116,7 @@ pub struct Config { /// verified-commitment-trees path below the last checkpoint: per-block Sapling/Orchard /// roots are verified against the committed headers and folded into the anchor set and /// history tree, skipping the per-block frontier recompute. The - /// `consensus.disable_vct_fast_sync` setting is mirrored into state to keep checkpoint sync + /// `consensus.vct_fast_sync` setting is mirrored into state to keep checkpoint sync /// enabled while forcing the legacy per-block recompute. /// /// Skipped in serde because it is not an independent state setting — it tracks the @@ -124,17 +124,17 @@ pub struct Config { #[serde(skip)] pub checkpoint_sync: bool, - /// Mirror of `consensus.disable_vct_fast_sync`, set by zebrad at startup. + /// Mirror of `consensus.vct_fast_sync`, set by zebrad at startup. /// - /// This keeps `consensus.checkpoint_sync` enabled while forcing the legacy per-block - /// Sapling/Orchard tree recompute in both Archive and Pruned storage modes. Set to `false` - /// by default: checkpoint sync uses VCT fast sync on networks with embedded handoff - /// frontiers. + /// When `true` (the default), checkpoint sync uses the verified-commitment-trees fast path on + /// networks with embedded handoff frontiers. Set to `false` to keep `consensus.checkpoint_sync` + /// enabled while forcing the legacy per-block Sapling/Orchard tree recompute in both Archive + /// and Pruned storage modes. /// /// Skipped in serde because users configure this alongside `consensus.checkpoint_sync`, not /// as an independent state setting. #[serde(skip)] - pub disable_vct_fast_sync: bool, + pub vct_fast_sync: bool, /// Whether to delete the old database directories when present. /// @@ -429,7 +429,7 @@ impl Default for Config { should_backup_non_finalized_state: true, enable_zakura_header_seed_from_committed_blocks: false, checkpoint_sync: true, - disable_vct_fast_sync: false, + vct_fast_sync: true, delete_old_database: true, storage_mode: StorageMode::default(), debug_stop_at_height: None, @@ -452,7 +452,7 @@ mod tests { #[test] fn storage_mode_deserializes_from_documented_toml() { assert!( - !Config::default().disable_vct_fast_sync, + Config::default().vct_fast_sync, "VCT fast sync is enabled by default when checkpoint sync and embedded frontiers are available" ); @@ -481,8 +481,8 @@ mod tests { let serialized = toml::to_string(&Config::default()).expect("state config serializes"); assert!( - !serialized.contains("disable_vct_fast_sync"), - "disable_vct_fast_sync is configured under [consensus], not [state]" + !serialized.contains("vct_fast_sync"), + "vct_fast_sync is configured under [consensus], not [state]" ); } } diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index 6009f920731..50357f2596d 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -518,7 +518,7 @@ impl FinalizedState { let vct = VctState::from_config( config.checkpoint_sync, - config.disable_vct_fast_sync, + config.vct_fast_sync, network, db.clone(), ); @@ -583,7 +583,7 @@ impl FinalizedState { "this database was previously synced in verified commitment tree mode that was \ interrupted below the checkpoint handoff height. the fast path that supplies \ the verified roots needed to resume it is disabled. Set \ - `consensus.checkpoint_sync = true` and `consensus.disable_vct_fast_sync = false` to \ + `consensus.checkpoint_sync = true` and `consensus.vct_fast_sync = true` to \ finish the fast sync, or delete the cache directory and re-sync from genesis" ); } diff --git a/zebra-state/src/service/finalized_state/tests/prop.rs b/zebra-state/src/service/finalized_state/tests/prop.rs index e38e12df8b7..2e6a533c78a 100644 --- a/zebra-state/src/service/finalized_state/tests/prop.rs +++ b/zebra-state/src/service/finalized_state/tests/prop.rs @@ -1345,7 +1345,7 @@ fn vct_mode_switches_continue_from_safe_boundaries() -> Result<()> { } let manual_config = Config { - disable_vct_fast_sync: true, + vct_fast_sync: false, ..fast_config }; let mut manual = FinalizedState::new(&manual_config, &network, #[cfg(feature = "elasticsearch")] false); @@ -1368,7 +1368,7 @@ fn vct_mode_switches_continue_from_safe_boundaries() -> Result<()> { let manual_prefix_config = Config { cache_dir: manual_to_fast_dir.path().to_path_buf(), ephemeral: false, - disable_vct_fast_sync: true, + vct_fast_sync: false, ..Config::default() }; { @@ -1382,7 +1382,7 @@ fn vct_mode_switches_continue_from_safe_boundaries() -> Result<()> { } let fast_suffix_config = Config { - disable_vct_fast_sync: false, + vct_fast_sync: true, ..manual_prefix_config }; let mut fast_suffix = FinalizedState::new(&fast_suffix_config, &network, #[cfg(feature = "elasticsearch")] false); diff --git a/zebra-state/src/service/finalized_state/vct.rs b/zebra-state/src/service/finalized_state/vct.rs index 1c5d57fdd4d..ba0d6bb5e88 100644 --- a/zebra-state/src/service/finalized_state/vct.rs +++ b/zebra-state/src/service/finalized_state/vct.rs @@ -3,7 +3,7 @@ //! This module holds the embedded-frontier plumbing and run counters for the //! verified-commitment-trees fast path. On networks with an embedded handoff frontier, //! the default source is the peer `tree_aux` source. `checkpoint_sync = false` or -//! `consensus.disable_vct_fast_sync = true` selects legacy recompute. +//! `consensus.vct_fast_sync = false` selects legacy recompute. //! //! [`super`] (`finalized_state.rs`) holds only the commit-path hook (the checkpoint //! handoff write and the fast-sync marker); everything about *where the data comes @@ -58,7 +58,7 @@ pub enum FinalFrontiersValidationError { /// /// A checkpoint-trusting sync (`checkpoint_sync = true`) uses the peer `tree_aux` source by /// default on networks with embedded final frontiers; `checkpoint_sync = false` or -/// `disable_vct_fast_sync = true` opts out to the legacy per-block recompute (no VCT state). +/// `vct_fast_sync = false` opts out to the legacy per-block recompute (no VCT state). #[derive(Debug)] pub(crate) struct VctState { /// Fast mode: skip the per-block frontier recompute and fold the source's roots @@ -92,15 +92,15 @@ enum SourceMode { /// unit-testable without touching embedded-frontier files. The fast verified path /// (peer source) is the default whenever the node syncs under checkpoint trust and /// the network has an embedded handoff frontier. `checkpoint_sync = false` or -/// `disable_vct_fast_sync = true` selects the legacy recompute; a network with no embedded +/// `vct_fast_sync = false` selects the legacy recompute; a network with no embedded /// frontier also falls back to legacy. Storage mode (Archive vs. Pruned) is orthogonal and not /// an input here. fn select_source_mode( checkpoint_sync: bool, - disable_vct_fast_sync: bool, + vct_fast_sync: bool, has_embedded_frontiers: bool, ) -> SourceMode { - if !checkpoint_sync || disable_vct_fast_sync || !has_embedded_frontiers { + if !checkpoint_sync || !vct_fast_sync || !has_embedded_frontiers { SourceMode::Legacy } else { SourceMode::Peer @@ -109,14 +109,14 @@ fn select_source_mode( impl VctState { /// Build the committer state from `checkpoint_sync` (the mirror of - /// `consensus.checkpoint_sync`) and the `disable_vct_fast_sync` force-disable knob. + /// `consensus.checkpoint_sync`) and the `vct_fast_sync` knob. /// On networks with an embedded handoff frontier (Mainnet) a checkpoint-trusting sync - /// defaults to the peer (`tree_aux`) fast source; disabling checkpoint sync, setting the - /// force-disable knob, or using a network without an embedded frontier returns `None` for a - /// zero-overhead legacy committer that recomputes the trees per block. + /// defaults to the peer (`tree_aux`) fast source; disabling checkpoint sync, setting + /// `vct_fast_sync = false`, or using a network without an embedded frontier returns `None` for + /// a zero-overhead legacy committer that recomputes the trees per block. pub(super) fn from_config( checkpoint_sync: bool, - disable_vct_fast_sync: bool, + vct_fast_sync: bool, network: &Network, db: ZebraDb, ) -> Option> { @@ -125,7 +125,7 @@ impl VctState { // parsed value. let embedded = embedded_final_frontiers(network); - match select_source_mode(checkpoint_sync, disable_vct_fast_sync, embedded.is_some()) { + match select_source_mode(checkpoint_sync, vct_fast_sync, embedded.is_some()) { // Default: the peer (`tree_aux`) source on any network with embedded final // frontiers (Mainnet). Per-block roots arrive from peers into a shared cache // filled by the driver; the committer reads them per height and folds them in, @@ -379,24 +379,24 @@ mod tests { #[test] fn source_mode_precedence() { use SourceMode::*; - // Args are (checkpoint_sync, disable_vct_fast_sync, has_embedded_frontiers). - - // The default: a checkpoint-trusting sync uses the peer source wherever embedded - // frontiers exist (Mainnet). Storage mode (Archive/Pruned) is not an input, so this - // covers both Archive and Pruned. - assert_eq!(select_source_mode(true, false, true), Peer); - // `disable_vct_fast_sync = true` keeps checkpoint sync on but forces the legacy - // recompute, regardless of embedded frontiers. - assert_eq!(select_source_mode(true, true, true), Legacy); - assert_eq!(select_source_mode(true, true, false), Legacy); + // Args are (checkpoint_sync, vct_fast_sync, has_embedded_frontiers). + + // The default: a checkpoint-trusting sync with VCT fast sync on uses the peer source + // wherever embedded frontiers exist (Mainnet). Storage mode (Archive/Pruned) is not an + // input, so this covers both Archive and Pruned. + assert_eq!(select_source_mode(true, true, true), Peer); + // `vct_fast_sync = false` keeps checkpoint sync on but forces the legacy recompute, + // regardless of embedded frontiers. + assert_eq!(select_source_mode(true, false, true), Legacy); + assert_eq!(select_source_mode(true, false, false), Legacy); // `checkpoint_sync = false` also fully recomputes the trees: legacy, never peer, - // regardless of the force-disable knob or embedded frontiers. - assert_eq!(select_source_mode(false, false, true), Legacy); - assert_eq!(select_source_mode(false, false, false), Legacy); + // regardless of the fast-sync knob or embedded frontiers. assert_eq!(select_source_mode(false, true, true), Legacy); assert_eq!(select_source_mode(false, true, false), Legacy); + assert_eq!(select_source_mode(false, false, true), Legacy); + assert_eq!(select_source_mode(false, false, false), Legacy); // No embedded frontiers (e.g. Testnet): legacy, never peer, even under checkpoint sync. - assert_eq!(select_source_mode(true, false, false), Legacy); + assert_eq!(select_source_mode(true, true, false), Legacy); } #[test] diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/prune.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/prune.rs index 0e63bc5cde5..64d0f4453a3 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/prune.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/prune.rs @@ -1153,7 +1153,7 @@ fn reopening_fast_synced_database_in_archive_mode_succeeds() { // force-disable knob selects manual recomputation. Fast sync deletes nothing; the missing // historical trees are surfaced at the RPC boundary, not by refusing to reopen. let config = Config { - disable_vct_fast_sync: true, + vct_fast_sync: false, ..config }; let reopened = FinalizedState::new( @@ -1197,7 +1197,7 @@ fn reopening_fast_synced_database_in_pruned_mode_with_vct_disabled_succeeds() { // Pruning only removes historical raw transaction bytes; it does not make a completed // fast-sync marker unsafe to reopen with VCT force-disabled. let config = Config { - disable_vct_fast_sync: true, + vct_fast_sync: false, ..config }; let reopened = FinalizedState::new( @@ -1261,7 +1261,7 @@ fn reopening_interrupted_fast_sync_with_vct_disabled_panics() { let config = Config { cache_dir: dir.path().to_path_buf(), ephemeral: false, - disable_vct_fast_sync: true, + vct_fast_sync: false, ..Config::default() }; diff --git a/zebrad/src/commands/start.rs b/zebrad/src/commands/start.rs index a0e67b5d513..292b5768bfd 100644 --- a/zebrad/src/commands/start.rs +++ b/zebrad/src/commands/start.rs @@ -546,7 +546,7 @@ impl StartCmd { // State owns the VCT commit path, but users configure its checkpoint-sync controls // together under `[consensus]`. state_config.checkpoint_sync = config.consensus.checkpoint_sync; - state_config.disable_vct_fast_sync = config.consensus.disable_vct_fast_sync; + state_config.vct_fast_sync = config.consensus.vct_fast_sync; let (state_service, read_only_state_service, latest_chain_tip, chain_tip_change) = zebra_state::init( diff --git a/zebrad/tests/common/configs/v5.0.0-rc.3.toml b/zebrad/tests/common/configs/v5.0.0-rc.3.toml index 5b7a7662ec6..15711e822d8 100644 --- a/zebrad/tests/common/configs/v5.0.0-rc.3.toml +++ b/zebrad/tests/common/configs/v5.0.0-rc.3.toml @@ -42,7 +42,7 @@ [consensus] checkpoint_sync = true -disable_vct_fast_sync = false +vct_fast_sync = true [health] enforce_on_test_networks = false @@ -91,7 +91,6 @@ stream_open_rate_per_second = 16 [network.zakura.block_sync] fanout = 1 floor_peer_avoid_cooldown = "8s" -floor_watchdog_tick = "1s" initial_inflight_requests = 64 max_blocks_per_response = 1 max_inflight_block_bytes = 2147483648 diff --git a/zebrad/tests/common/configs/v5.0.0-rc.4.toml b/zebrad/tests/common/configs/v5.0.0-rc.4.toml index e04fc6b0f0d..ad798b22315 100644 --- a/zebrad/tests/common/configs/v5.0.0-rc.4.toml +++ b/zebrad/tests/common/configs/v5.0.0-rc.4.toml @@ -42,7 +42,7 @@ [consensus] checkpoint_sync = true -disable_vct_fast_sync = false +vct_fast_sync = true [health] enforce_on_test_networks = false @@ -91,7 +91,6 @@ stream_open_rate_per_second = 32 [network.zakura.block_sync] fanout = 1 floor_peer_avoid_cooldown = "8s" -floor_watchdog_tick = "1s" initial_inflight_requests = 64 max_blocks_per_response = 1 max_inflight_block_bytes = 6442450944