Skip to content

fix(sync): credit discovery peers only for verified progress - #1037

Open
evan-forbes wants to merge 6 commits into
mainfrom
security/verified-discovery-progress
Open

evan-forbes wants to merge 6 commits into
mainfrom
security/verified-discovery-progress

Conversation

@evan-forbes

Copy link
Copy Markdown
Contributor

Motivation

Nonempty inventory does not prove chain progress. A peer can return arbitrary hashes without supplying useful work. Repeated retry activity can also defeat a timeout that restarts on every loop iteration.

Solution

  • Credit discovery responses only after an admitted, previously unknown hash commits. Separate the advertising connection from the body supplier.
  • Bound local feedback by connection generation and apply outcomes in request order. Keep cancellation and local failures neutral. Expiry requests a cooldown without a misconduct strike.
  • Rotate eligible TCP discovery peers and source-less v2 discovery peers. Feedback capacity does not stop checkpoint acquisition.
  • Bound candidate evidence and reconciliation work. Keep feedback off the wire and cancel fanout futures with their owner.
  • Enforce an absolute deadline from the last verified commit across active retries, dispatch, discovery refresh, and waits.
  • Register a timer for pending-feedback and cooldown deadlines so an idle owner wakes without network activity.

Testing

The audit passes 100 combined sync tests and nine tracker tests. New regressions cover continuous registry failures without verified progress and idle-owner wakeups at both deadlines.

An earlier audit passed 115 peer-set tests and 141 discovery-filtered network tests, including three-node v2 rotation.

Strict clippy passed for the state, consensus, network, and node libraries and tests. Formatting and changelog checks passed.

Changelog

Adds a changelog fragment and parameter-ledger entries.

Release gates

Keep this PR in draft. Validate single-peer checkpoint completion, external commits, native handoff, sustained adversarial traffic, and expiry under local backpressure. V2 discovery rotates peers but does not use TCP's verified-progress strike tracker. The zcashd exemption and header watchdog heuristic remain.

Provenance

Ported unchanged from the private security review. It no longer needs to stay private. This PR is independent of #1036.

@evan-forbes evan-forbes added consensus-not-critical blocksync anything related to blocksync C-security p2p Legacy Zcash P2P stack labels Sep 17, 2026
@evan-forbes
evan-forbes marked this pull request as ready for review September 17, 2026 22:27
@evan-forbes
evan-forbes requested a review from a team September 17, 2026 22:27
@v12-auditor

v12-auditor Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Note

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

Open the full results here.

FindingSeverityDetails
F-281499 🟠 High
Round aborts erase peer accountability

A selected peer’s non-empty FindBlocks response is retained as discovery evidence when it contains previously unknown hashes, and response-arrival order determines the download prefix. An attacker controlling the eligible discovery fanout can answer first with nonexistent hashes, occupy the active lookahead, and prevent any advertised hash from committing until missing-block retries or the eight-minute verified-progress deadline abort the round. The outer error path then cancels downloads and replaces Discovery, dropping the unresolved feedback before its sixteen-minute expiry. DiscoveryFeedback::drop converts that unresolved outcome to NEUTRAL, which causes neither a stall strike nor an expiry cooldown, so the next round resets all retry state and can select the same peers again.

F-281500 🟡 Medium
Pending capacity bypasses stall strikes

A peer can fill its two feedback slots with syntactically valid responses containing previously unknown hashes that never commit. eligible() checks only the reprobe deadline, so a capacity-saturated peer remains in the FindBlocks rotation, while start() returns None and the request is still sent. Every later empty, malformed, or failed response therefore lacks a feedback token and cannot call no_progress() to add a strike. FIFO draining leaves the two retained entries pending for up to sixteen minutes, and expiry only applies a cooldown rather than a misconduct strike.

F-281501 🟡 Medium
Singleton replies evade discovery strikes

A TCP peer can return a successful one-hash FindBlocks response that the syncer has already proven unusable, yet avoid no_progress(). In obtain_tips, an already-known singleton yields no unknown suffix, and in extension a singleton other than the expected next hash is explicitly rejected; both call sites skip feedback failure exactly when raw_len == 1. The unresolved capability then drops to NEUTRAL, which the stall tracker ignores, so the peer never approaches the three-strike threshold or enters cooldown. Because singleton compatibility is already handled by accepting usable singleton responses and configured zcashd-compatible peers are separately exempted before feedback creation, the semantic no-progress result has no reason to remain neutral.

F-281502 🟠 High
Tip timeout bypasses peer accountability

Production block discovery wraps the peer service in a six-second Tower timeout, but the peer connection does not classify a missing response as ConnectionReceiveTimeout until its twenty-second request timer fires. If a peer accepts getblocks and stays silent, the outer timeout drops the peer-set response future first; dropping the client oneshot signals cancellation, and the connection returns to AwaitingRequest without an error. The still-pending DiscoveryFeedback is then dropped and becomes NEUTRAL, which produces neither a strike nor a reprobe cooldown. The connection becomes ready and remains eligible, so the same peer can repeat the behavior indefinitely.

F-281503 🟠 High
Invalid replies remain strike-free

The FindBlocks handler completes only for an inv containing exclusively block hashes. A peer can answer getblocks with another valid wire message, such as notfound, headers, or mixed/transaction inventory; the message is treated as unused or unrelated inbound traffic while the original handler remains pending. No PeerError reaches the new remote-failure classifier, and the six-second discovery timeout then cancels the request. Cancellation drops the feedback to NEUTRAL, so the connection survives without a strike or cooldown and can repeat the protocol-invalid response indefinitely.

F-281504 🟠 High
Header probes can be pinned

FindHeaders no longer uses the special oldest-selected discovery rotation or stall feedback; only FindBlocks updates last_selected and creates feedback. The legacy watchdog therefore sends its three header probes through ordinary P2C, which prefers lower-load peers, and prompt empty responders can remain favored over an honest peer with useful headers. Empty responses produce an ahead distance of zero and no strike, so the watchdog never reaches the threshold that activates legacy fallback. Attackers can relay an occasional valid block to reset the separate idle counter while keeping the victim arbitrarily far behind and preventing the connected honest peer from being selected for the recovery probe.

F-281505 🟠 High
Side chains refresh progress deadline

Reconciliation treats KnownBlock::SideChain exactly like finalized or best-chain membership: it calls discovery.committed(hash) and resets last_verified_progress. A discovery peer can submit a previously unknown hash for a consensus-valid block extending any retained non-finalized ancestor; state commits that block into a lower-work fork and later reports it as SideChain. Each such side-chain commit refreshes the absolute best-chain stall deadline even though the selected tip did not advance. The same call marks the advertiser verified, and the stall tracker removes all of that peer’s accumulated no-progress strikes.

Analyzed 15 files, diff cc2b5b8...d153f9d.

The audit of #1037 reported that a discovery peer can avoid every consequence
the pull request introduces.

A dropped `DiscoveryFeedback` completes as neutral, which records nothing. The
syncer cancels a discovery request after six seconds, long before the connection
classifies a silent peer as a receive timeout, so a peer that accepts
`getblocks` and then stays quiet — or answers with an unrelated message the
handler never completes — kept its slot in the rotation and could repeat that
forever. `PendingDiscovery` now holds the evidence across the request and
expires it on cancellation, which rotates the peer out for the reprobe delay
without charging a misconduct strike.

Aborting a sync round replaced the whole `Discovery` record, dropping its
retained evidence to neutral, so peers that filled the round with hashes that
never committed were eligible again immediately. `Discovery::abandon` resolves
the retained responses instead.

Reconciliation treated a side-chain commit as verified progress. A side chain
does not advance the best chain, so it no longer postpones the progress
deadline; it still credits the peer that supplied the block.
Narrowing discovery feedback to `FindBlocks` removed stall accountability from
`FindHeaders` entirely. The legacy watchdog's header probes could then be
answered emptily forever without recording anything, so the watchdog never
reached the threshold that activates legacy fallback, and a node could be held
arbitrarily far behind.

Headers never commit a block, so a header probe still cannot credit a peer with
verified progress: its feedback is never attached to a response, and a non-empty
answer clears nothing. An empty answer while this node is behind the tip is
proven no-progress, which is what the threshold counts. Header probes also keep
using ordinary peer selection rather than the oldest-selected discovery
rotation.
Main's #940 added verification-timeout retry budgets and their tests at the same
place in the sync test vectors as this branch's verified-progress deadline tests.
Both sets are kept.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocksync anything related to blocksync C-security consensus-not-critical p2p Legacy Zcash P2P stack

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant