mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Fixes the live mainnet killer: a slipped validator (e.g. 192 behind) receives verified
α-of-K certs for blocks it TRACKS but whose intermediate ancestors it LACKS. The finalize
fold returns ErrAncestorNotTracked; HandleIncomingCert logged "REFUSED ... (behind; fetch
and retry)" and returned WITHOUT calling requestCatchup — so the node never self-healed,
stayed effectively n−1 at zero margin, and any flap dropped the fleet below α and stalled
finality. Clean restart did not fix it (boots, re-reads its head, re-stalls). The only fetch
trigger in HandleIncomingCert was gated behind "cert's OWN block untracked" (topology.go
!exists arm), never the missing-INTERMEDIATE-ancestor case.
Fix (surgical, two hunks):
- ledger.go: pathFromTip now returns a TYPED *AncestorNotTracked carrying the specific
untracked ancestor id (Missing) and the certified target (Target). errors.Is(err,
ErrAncestorNotTracked) still holds via Unwrap, so every existing classifier is byte-for-
byte unchanged; the plain sentinel only named the id inside a format string, which is why
the receiver could log the defer but never target a fetch.
- topology.go: HandleIncomingCert's error arm errors.As the typed error and issues exactly
one requestCatchup(Missing, EmptyNodeID). Gated on the cert having VERIFIED (a forged cert
can never make us fetch arbitrary ids); claimCatchupLocked rate-limits; peer selection is
the node layer's job. The node-layer fetch machinery (networkCatchup.RequestAncestors →
requestContext → GetAncestors → AcceptCatchupBlock) is fully wired and serves the finalized
gap oldest-first, re-applying the deferred cert — it was simply never invoked on this path.
Result: a behind node self-heals to the tip (requestCatchup → gap serve → finalize), restoring
5/5 and margin, so a flap no longer drops below α. This is the launch-gate invariant on the
current engine and is prerequisite plumbing for any future engine (same Finalize fold).
Tests (fail-on-old/pass-on-new, -race clean):
- TestFinalize_MissingAncestor_TypedError: the fold yields *AncestorNotTracked with the
correct Missing id; errors.Is(ErrAncestorNotTracked) preserved.
- TestSelfHeal_CertMissingIntermediateAncestor_TriggersFetch: a verified cert for a tracked
block with a missing intermediate ancestor fires exactly ONE ancestor fetch targeting the
missing id (pre-fix: 0 — logged REFUSED, never self-healed).
Co-authored-by: Hanzo Dev <[email protected]>