Skip to content

fix(sync): replenish bounded header lookahead before the backlog drains - #1034

Closed
evan-forbes wants to merge 4 commits into
fix/header-continuation-capfrom
fix/bounded-header-refill
Closed

evan-forbes wants to merge 4 commits into
fix/header-continuation-capfrom
fix/bounded-header-refill

Conversation

@evan-forbes

Copy link
Copy Markdown
Contributor

Motivation

Header sync waits for admitted header lead to fall from 4,000 to 2,000 before refilling. In a traced checkpoint interval at heights 1.4M–1.5M, the node held 2,733 downloaded bodies against 2,856 admitted headers on average, despite keeping all 401 verifier submissions occupied. Most unused backlog capacity had no admitted header to download.

Keep the backlog supplied as block execution becomes faster. This change does not claim a measured throughput gain.

Solution

  • Reopen refill when one checkpoint range plus its successor fits in the existing window: 401 returned credits, or a header lead of 3,599.
  • Publish completed selected-chain batches without waiting for the backlog to drain or chasing newly returned credits.
  • Reconsider cached targets when verified progress reopens capacity, including a final one-header suffix, without requiring a status refresh or reanchor.
  • Preserve the 4,000-header window, aggregate chunk reservations, durable graph bounds, negotiated wire limits, and body memory budgets.

The active header graph is memory-resident. The design therefore retains explicit bounds rather than assuming disk-backed headers have negligible memory cost.

Stacked on #1032. This policy supersedes #1016's 2,000-header publication threshold; the combined stack must resolve that overlap explicitly and preserve its moving-headroom regression intent.

Testing

  • 178 header-sync tests passed.
  • Network Clippy passed for all targets with warnings denied.
  • Workspace formatting and diff checks passed.
  • Regression coverage includes refill boundaries, shared claims, final partial targets, moving headroom, actual prefix preparation, and verified-progress wakeups without duplicate locator work.

The current genesis run remains the baseline. More frequent header transitions may cost CPU. The experiment plan requires matched checkpoint-only comparisons, full backlog distributions, stage latency, RSS, graph growth, and a throughput regression gate before acceptance.

Specifications & References

Implementation and experiment plan

Follow-up Work

Integrate with the combined stack, resolve the #1016 policy overlap, and run the bounded-refill comparison after the baseline completes. The separate large-body delivery deficit still requires congestion and bandwidth diagnosis.

@czarcas7ic
czarcas7ic marked this pull request as ready for review September 17, 2026 20:06
@v12-auditor

v12-auditor Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Note

Complete: Audit complete. V12 found eight issues worth reviewing.

Open the full results here.

FindingSeverityDetails
F-281223 🟡 Medium
Forged heights force one-header admissions

A peer can advertise a real distant selected_tip_hash while setting selected_tip_height at or below the victim's current header tip, because status handling checks only that the work-anchor height does not exceed the advertised tip height. The refill calculation then saturates target_remaining to zero, classifying the distant target as a final partial target and bypassing the new 401-credit coalescing guard whenever even one body credit is available. An incomplete response may return a valid child above the advertised height because response handling validates the target hash and locator ancestry but never bounds the inferred staged height by selected_tip_height; once that response consumes the available credit, it is sealed as a TargetPrefix. The new verified-progress wakeup reuses the cached forged status, so each subsequent body commit can automatically trigger another one-header preparation and admission without a fresh status message. Timely consensus-valid responses reset the unproductive-request tracking and do not trigger peer misbehavior defenses.

F-281224 🟡 Medium
Stale claims suppress the refill wakeup

observe_latest_committed_snapshot captures the aggregate claimed-header count and computes the refill closed-to-open edge before retiring obsolete work. A verified-only snapshot does not change header authority, but it makes any active body-authorized VCT repair stale because the verified generation changed; that repair's reservation is therefore included in both capacity calculations and released only afterward. For example, with a distant cached target, body headroom increasing from 400 to 401, and a stale 401-header repair reservation, both pre-retirement calculations return zero even though retirement immediately makes all 401 new credits usable. The snapshot is then installed with refill_reopened still false, so cached ordinary targets are not reconsidered. Later body snapshots see the already-open state and cannot recreate the missed zero-to-positive edge, while maintenance and repair scheduling do not replay ordinary cached targets.

F-281225 🟡 Medium
Withheld reservations fragment header commits

At the new 401-credit refill boundary, two peers advertising distinct distant targets can reserve essentially the entire shared opening before an honest target starts: ordinary requests are capped at 250 credits, so attacker-controlled limits of 250 and 150 leave only one credit, or 250 and 151 consume all 401 until the next body commit returns one more. When the honest peer then returns one valid header, durable_prefix_full observes zero global capacity because the attackers' unrelated withheld reservations are included in claimed, and converts that one-header incomplete response into a TargetPrefix. Admitting the honest header advances header_generation, which retires and cancels the attackers' requests as snapshot-obsolete rather than timing them out. Their cached statuses are immediately reconsidered, allowing them to reserve the next opening again without accruing unproductive-request strikes. This cycle is practical with two peers after the threshold reduction; the prior 2,000-credit opening required about eight 250-credit reservations to create the same one-credit fragmentation.

F-281226 🟡 Medium
Distinct tips duplicate prefix validation

Active-work uniqueness is keyed by each peer's advertised final target hash, but incomplete prefixes are later rebound to their staged tip. A Sybil actor can therefore advertise eight distinct distant hashes and have every peer return the same valid 401-header selected-chain suffix with complete = false; at a body lag of 401, the 3,599-credit window accommodates all eight 401-header claims. The changed checkpoint predicate independently marks every request ready because each is a normal integrated extension of the current selected tip and has more than 400 entries. Since target equality is enforced only for complete responses, all eight requests are rebound to the same staged prefix and dispatched as separate preparation jobs. Only one insertion can ultimately advance the generation, so the other expensive preparations and apply attempts are redundant, and the distinct cached advertisements become eligible again after the generation change.

F-281227 🟡 Medium
Resource-stalled prefixes revalidate indefinitely

An ordinary target that returns ResourceStalled is retired without being marked complete, backed off, or parked until the reported state version changes. Target admission also ignores the committed resource_stalled alarm, so a peer can advertise the same uncompleted target again and resend the same valid selected-chain prefix while graph pressure is unchanged. The changed shortcut immediately prepares any selected normal prefix over 400 entries even when body lag is well above the old one-checkpoint guard, causing full proof-of-work preparation before the planner repeats the known retention refusal. Because neither status admission nor the ordinary result path suppresses the target while the alarm remains active, the peer can repeat this expensive cycle with fresh status messages. The graph bound remains enforced, but the refusal path itself becomes attacker-driven work.

F-281228 🟡 Medium
Retirement releases live validation leases

Early prefix publication moves peer-controlled header batches into asynchronous preparation and application futures, but the only 4,000-header accounting lease remains attached to the peer-work queue entry rather than the local operation. Ordinary work retirement on disconnect, request deadline, or a sibling's header-generation advance removes that queue entry and drops its reserved/owned leases even though the already-dispatched future remains in pending_port_operations. Those futures have no cancellation handle and continue validating the moved headers or waiting on serialized state application; their eventual callback is merely ignored if the matching active record is gone. The released budget can immediately be allocated to fresh advertisements or a reconnected peer, allowing another full round while stale local operations still retain their data and work. The new 401-header early-publication path makes such overlapping local operations routine before a complete target or full 4,000-entry page has finished.

F-281229 🟡 Medium
Transient locator failure loses refill forever

The new verified-progress wakeup is a one-shot zero-to-positive edge rather than a latched scheduling condition. For a distant target, body headroom changes from 400 to 401 only once; that snapshot reconsiders the cached target and dispatches a locator query. If the local locator read times out or returns unavailable, dispatch fails, or the locator completion races with another reservation and sees zero capacity, the unstarted target is removed without retry state. Later body commits increase headroom from 401 upward, but both the old and new snapshots now evaluate as open, so refill_reopened never fires again. The cached status remains present yet unused until a fresh peer status or unrelated header-authority change arrives.

F-281230 🟡 Medium
Verified resets exceed the header window

Ordinary header requests are authorized by header generation and finalized anchor, so they survive a committed snapshot that resets verified_best backward while leaving the selected header graph unchanged. A request can reserve the newly allowed 401-header refill at a 3,599-header lead; if full state then resets the verified tip backward before the response arrives, the current integrated body window shrinks or closes but the reservation is retained. A later 401-header incomplete response still becomes a bounded prefix and passes the header-only preparation/application authority checks, because no current-snapshot body-window limit is revalidated before admission. The selected header lead becomes 3,599 + reset_depth + 401, exceeding the intended 4,000-header integrated limit by the reset depth. The peer needs only valid linked headers and favorable timing around a legitimate verified-chain reset.

Analyzed one file, diff 71aa7f9...646fbd3.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-17T23:04:41.276784Z c0f1c2b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 646fbd3814

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/zakura-network/src/zakura/header_sync/reactor.rs Outdated
evan-forbes and others added 2 commits September 17, 2026 15:42
Give the refill batch one name so the window gate and the publication
trigger cannot drift apart.

State that the batch bound applies to the window rather than to the
grant. Outstanding claims can leave less than a batch free, and the
reactor still sends that smaller top-up to keep the body backlog
supplied.

Move the closed-to-open capacity test into its own predicate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0f1c2bbe2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2921 to +2922
Self::request_header_prefix_remaining(old, claimed_before, target) == 0
&& Self::request_header_prefix_remaining(new, claimed_after, target) > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wake cached targets when repair retirement frees the peer

When the only supplier is occupied by a VCT repair while the window still has some numeric capacity (for example, 500 credits with a 401-header repair claim), its ordinary target is cached but cannot be staged because the peer is already active. The next verified-generation update retires that repair and frees both the peer and its claim, but this predicate sees a positive-to-positive capacity change and does not reconsider the target, delaying refill until another peer status arrives (30 seconds by default). This is distinct from the earlier claim-ordering case: the fresh failing condition is that peer availability changes while capacity never reaches zero, so retirement should also wake cached work when it frees an occupied peer.

Useful? React with 👍 / 👎.

Comment on lines +2920 to +2922
let target = status.selected_tip_height;
Self::request_header_prefix_remaining(old, claimed_before, target) == 0
&& Self::request_header_prefix_remaining(new, claimed_after, target) > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate target height before using the partial-target wakeup

When an untrusted peer advertises selected_tip_height one block above the local tip while selected_tip_hash actually names a longer target, the partial-target exception grants one header even though the window lacks a full batch. After the peer returns a valid incomplete page and one body is verified, this predicate trusts the same unauthenticated height to wake the target again; the peer can therefore force repeated one-header preparation and durable-admission cycles instead of the intended 401-header coalescing. Validate the advertised height against the response ancestry when that inconsistency becomes observable, or exclude unverified partial targets from sub-batch wakeups.

AGENTS.md reference: AGENTS.md:L149-L149

Useful? React with 👍 / 👎.

evan-forbes added a commit that referenced this pull request Sep 23, 2026
Combine the #1032 and #1034 changelog fragments into one, named after
the PR that supersedes them, and point the params.md row at #1136.
@evan-forbes

Copy link
Copy Markdown
Contributor Author

Superseded by #1136, which carries #1032 and #1034 onto main as one PR.

ValarDragon pushed a commit that referenced this pull request Sep 25, 2026
…ns (#1136)

* fix(sync): restore header continuation capacity as credits return

(cherry picked from commit e9f47af)

* docs: record header continuation capacity fix

(cherry picked from commit 71aa7f9)

* fix(sync): replenish bounded header lookahead before the backlog drains

(cherry picked from commit 00d0429)

* docs: record bounded header refill policy

(cherry picked from commit 646fbd3)

* refactor(sync): name the refill batch and the refill wake predicate

Give the refill batch one name so the window gate and the publication
trigger cannot drift apart.

State that the batch bound applies to the window rather than to the
grant. Outstanding claims can leave less than a batch free, and the
reactor still sends that smaller top-up to keep the body backlog
supplied.

Move the closed-to-open capacity test into its own predicate.

(cherry picked from commit 46c3879)

* fix(sync): wake header refill after retiring repair claims

(cherry picked from commit c0f1c2b)

* docs: merge changelog fragments into #1136

Combine the #1032 and #1034 changelog fragments into one, named after
the PR that supersedes them, and point the params.md row at #1136.

* fix(sync): keep header capacity charged through local operations

Address the v12 audit findings F-288244 and F-288245 and a review
comment on the refill wakeup:

- Prepare and apply operations now retain their capacity leases until
  their completion callbacks run, so a disconnected supplier cannot
  release credits while its local validation still holds the headers.
- Port completion handling now observes the latest committed snapshot
  before the reactor handles another event, so another request cannot
  reuse released credits against the old header lead.
- Snapshot refill wakeups now reconsider only the targets whose capacity
  reopened, instead of querying locators for every cached target.

---------

Co-authored-by: Adam Tucker <adamleetucker@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants