Skip to content

Head catch-up wedges behind tip: gossip-fresh skip (#71) is not lag-aware #83

Description

@Reiers

Problem

Split out from #82 (bug 2). This is the underlying disease behind Beck's bridge-off prove failure, and it affects every bridge-off node, not just proving.

A bridge-off node can wedge its head ~10-20 epochs behind the real chain tip and stay there. Observed (Beck, v1.8.4-m, mainnet 2026-06-30):

[gossip-block] sub-rcv=125 installed=11 skipped=43 backfilled=54 lastEpoch=6149489
[sync] polls=21 advances=1 head=6149469

Gossip sees the tip at 6149489 but head is stuck at 6149469, and the polling Sync advances=1 over 22 polls.

Root cause

The #71 gossip-fresh skip in chain/header/store/sync.go::pollAndApply is not lag-aware:

if gossipFresh != nil && s.store.HeadEpoch() >= 0 && gossipFresh() {
    s.stats.SkippedGossipFresh++
    return nil   // skips the entire catch-up poll
}

GossipFresh() returns true if the gossip ingestor installed any block recently. But net/blockingest skips blocks arriving at head+N with N>1 (defers them to Sync). So gossip can be advancing the head a little (GossipFresh=true) while still being far behind the tip because it skips every far-ahead block. Sync then skips its catch-up poll on the assumption "gossip's got it" — but gossip can't close the gap and Sync won't run. Deadlock: head sticks ~20 behind.

The #71 optimization is correct in steady state (don't hammer Glif when gossip is keeping head at the tip). It's wrong when gossip is fresh-but-lagging.

Fix

Make the gossip-fresh skip lag-aware. Skip the catch-up poll only when gossip is fresh AND the store head is within a small tolerance of the chain tip. When the store is far behind, run the catch-up poll regardless of gossip freshness — that is exactly what Sync exists for.

To avoid paying an RPC HeadEpoch() just to measure lag (the thing #71 is avoiding), use the gossip-observed tip the ingestor already tracks (lastEpoch in net/blockingest stats): skip only if storeHead >= gossipObservedTip - tolerance. No extra RPC call.

  • Add a GossipObservedHead func() abi.ChainEpoch (or extend GossipFresh to a lag-reporting callback) wired from net/blockingest.
  • Skip-poll condition becomes: gossipFresh() && storeHead >= gossipObservedHead() - SkipTolerance (SkipTolerance small, e.g. 2-3 epochs).
  • When lagging, fall through to the normal CatchUpChunk poll, which already paces contiguous catch-up.

Secondary (optional, separate): escalate gossip-skipped head+N blocks to immediate targeted backfill rather than waiting for the next Sync poll, so catch-up is faster under healthy gossip.

Acceptance

Reported via Beck v1.8.4-m. Parent: #82. Related: #71, #50, #53.

Metadata

Metadata

Assignees

No one assigned

    Labels

    architectureTrust model / swarm designbugSomething isn't workingp1High priority

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions