praos: heal non-contiguous candidate chains instead of looping on orphan
A passive follower could wedge permanently once at the live tip: after a
deep rollback the peer re-announces its chain, but our cached copy has a
gap (the peer fragment skipped a header, and that gap block was never
fetched because it never appeared in `missing` — it isn't in the
fragment). `select_chain` then classified the strictly-better candidate
as a non-contiguous / fork-mismatch orphan, cleared the fragment, and
requested re-intersection — which the peer answers by rolling back to our
tip and re-announcing, looping ~1.4x/s with the tip frozen.
Primary fix (gap-fill): when a strictly-better candidate's cached chain
is non-contiguous, fetch the contiguous range [common ancestor ->
candidate tip] from the announcing peer instead of giving up. A range
request needs only the two endpoints; the peer streams every intermediate
block, healing the gap so a later pass switches. Genesis-rooted
ancestors still fall back to the orphan verdict (can't anchor a range at
Origin).
Secondary safety net (fork-tip prune): the periodic gap-bridge could also
fixate on an abandoned fork tip left far ahead in chain_tree after the
rollback — no connected peer offers it, so the bridge re-issued a peerless
(peer_count=0) no-op fetch forever. `ChainTree::remove_fork_tip` drops
such an unreachable best_tip and recomputes best_tip; retry_select_chain
prunes rather than emit a peerless fetch.
Tests: select_chain_heals_noncontiguous_cached_candidate_via_range_fetch
and retry_prunes_unreachable_best_tip_instead_of_peerless_fetch (both
verified to fail without the respective change). 300 pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>