Skip to content

v1.8.5: head catch-up + bridge-off proving + fork-choice & eclipse hardening (#50 #79 #80 #81 #82 #83)#84

Merged
Reiers merged 12 commits into
mainfrom
fix/83-lag-aware-gossip-skip
Jun 30, 2026
Merged

v1.8.5: head catch-up + bridge-off proving + fork-choice & eclipse hardening (#50 #79 #80 #81 #82 #83)#84
Reiers merged 12 commits into
mainfrom
fix/83-lag-aware-gossip-skip

Conversation

@Reiers

@Reiers Reiers commented Jun 30, 2026

Copy link
Copy Markdown
Owner

v1.8.5 — head catch-up + bridge-off proving + fork-choice & eclipse hardening

Came out of Beck's v1.8.4-m test (PDP prove failing cannot draw randomness from future epoch) and the zenground0/FilOz fil-curio-dev thread on head agreement / fork selection. Scope expanded to also close the parked bridge-off availability work (#50/#52) and the first eclipse-hardening piece (#80).

Fixes

Added

Changed

Tests

  • New: lag-aware skip (run vs skip), nil-source Sync no-op, bridge-off future-epoch fast-fail, fork-choice rejects-lighter/accepts-heavier, protected-peer un-evictable floor, prefetch-on-send warmer (5 cases).
  • Full suite green across all 43 packages (-short, LANTERN_OFFLINE=1); build + vet + gofmt clean. (The one network test that needs :1234 collides with the local dev daemon only.)

Closes #50, #79, #80 (part 1), #81, #82, #83. Supersedes PR #52 (merged in). Follow-ups: #80 head-source diversity quorum, #79 periodic re-quorum.

Reiers added 12 commits June 22, 2026 01:03
…(UN-DEPLOYED, needs supervised bridge-off verify)

When eth_sendRawTransaction publishes a tx locally (#45 Stage 4), start a
background StateSearchMsg poll for its message CID on a standalone,
generous-deadline context. That background search drives the embedded
Bitswap source to pull the freshly-produced message + AMT + receipt blocks
into cache as soon as the tx lands. By the time the client's own short-
deadline receipt poll runs, the blocks are warm and the receipt resolves
locally instead of racing (and losing to) a cold cross-peer Bitswap fetch
inside the poll window — the residual that kept #50 open.

- new ChainAPI.OnSentTx hook, fired non-blocking after a successful local
  MpoolPush (mirrors the #44 OnLocalMiss adaptive-warming pattern)
- pkg/daemon sendWarmer: per-tx background warm loop, dedup, concurrency
  cap (64), self-bounded (stop on found / 10m max), bound to daemon root ctx
- purely additive + read-only: nil hook = unchanged behavior; never affects
  the send result; never publishes or mutates state
- 5 unit tests (stop-on-found, dedup, concurrency cap, ctx-cancel, nil-safe),
  pass under -race; full pkg/daemon + rpc/handlers suites green

NOT deployed to cc-smoke. Needs live bridge-off verification in a supervised
daytime window before merge (the engine builds + unit-tests clean, but the
real proof is a bridge-off write-confirm cycle showing zero
'net/bitswap: context canceled' receipt misses).
Formatting-only; no behavioral change. The branch CI failed on gofmt drift
(struct field alignment in sendWarmer). No logic touched.
…hind tip

The #71 gossip-fresh skip suspended the polling Sync's catch-up whenever
gossip installed any block recently. A node whose gossip is fresh-but-lagging
(installing some blocks while skipping head+N>1 blocks it can't backfill)
would then have neither path drive to the tip: gossip skips the far blocks,
Sync defers to gossip. Head wedges ~10-20 epochs behind (Beck, v1.8.4-m),
which also fails PDP prove with 'cannot draw randomness from future epoch'
when the prove randEpoch lands above the lagging head.

Make the skip lag-aware: blockingest exposes ObservedHead() (highest
installed epoch = gossip's view of the tip); Sync skips only when the store
head is within SkipLagTolerance (default 2) of that observed tip, and runs
the catch-up poll when fresh-but-lagging. Uses the gossip-observed tip, so
no extra upstream HeadEpoch RPC - #71's 429-protection is preserved at the
tip. Wired in both pkg/daemon and cmd/lantern. Two regression tests.
…dge-off)

tipsetForRandomness compared randEpoch against the frozen boot anchor
(c.Trusted.Epoch, never advances - see #48) instead of the live header-store
head, so a PDP prove asking for randomness at its challenge epoch could trip
'cannot draw randomness from future epoch' even on a node that had reached
that epoch. The documented fallback in StateGetRandomnessDigestFromBeacon was
bridge-only, so bridge-off nodes (the stock-Curio target) just failed and
burned the prove task's retry budget (Beck, v1.8.4-m, dataset 1294).

Now the ceiling is the live head, and when randEpoch is within a small window
above it (normal sync catch-up) we wait briefly (bounded 20s) for the header
sync to reach it, then draw locally - no bridge needed. Genuinely-future
epochs still error promptly. Pairs with #83 (which keeps the head at the tip
in the first place).
The gossip head ingestor advanced head on height-fence + parent-linkage
only, so an attacker who eclipses the peer table could feed parent-linked,
height-advancing blocks on a valid-but-lighter fork and walk a node onto it
(zenground0/FilOz, fil-curio-dev 2026-06-30). Content addressing doesn't
catch it: the attacker's blocks hash fine, they're just not canonical.

Apply Filecoin's actual fork-choice rule: adopt a candidate as head only
when its ParentWeight strictly exceeds the current head's. A real descendant
always has strictly greater weight; a competing lighter fork has lower/equal
weight and is now rejected (counted as rejectedLighter for observability).
Pure header arithmetic - no proof verification, no ffi, no one-binary impact.
Raises the eclipse cost from 'spin up N sybil peers' to 'out-weight the real
chain' (control real storage power).

Does NOT fully close the unfinalized-tip split against an adversary with real
power - that's finality (F3). Periodic live-head re-quorum (the other half of
 #79) remains a follow-up.
Section 2.1 claimed election-proof and weight were re-verified on every
header back to genesis. That overstated the code: weight fork-choice only
just landed (#79), and election-proof is presence-checked not VRF-verified
(full proof verification needs filecoin-ffi, which the no-CGo design
excludes). Rewrote to state precisely what IS verified (CID integrity, sig
shape, parent linkage, heaviest-ParentWeight fork choice) vs what is NOT
(election-proof VRF, winning-PoSt, message re-execution) and why, and to
point at F3 + the boot quorum as what actually carries head trust.
Part 3 of #50. The embedded daemon hardcoded Glif as the polling Sync head
source + cold-block fallback with no opt-out, so a bridge-off node silently
fell back to Glif whenever gossip stalled - a hidden third-party dependency.

- Config.FallbackRPC overrides the upstream RPC URL (e.g. operator's own
  Forest), defaulting to the network Glif URL (unchanged behavior).
- Config.NoFallbackRPC removes the upstream RPC entirely: head comes only
  from gossipsub, cold blocks only from gateway+Bitswap. A gossip stall then
  surfaces as a stalled head (observable) instead of a silent Glif fetch.
  Refuses to start if libp2p is disabled (no head source at all).

Wired so syncSrc/backfillSrc are truly-nil interfaces (not typed-nil) when
NoFallbackRPC, and hstore.Sync.pollAndApply guards nil src as a no-op so
gossip drives the head alone without nil-panicking. Test: nil-source Sync is
a safe no-op leaving head intact.

(Part 1 = PR #52 prefetch-on-send, merged. Part 2 = msgsearch
bitswap-with-retry, already shipped in the #77 bridge-off parity work.)
Part 1 of #80 (eclipse hardening). Host.ProtectPeers marks the trusted
bootstrap/beacon/direct-peer set as connmgr-protected so the trim path can
never evict them. An attacker flooding the node with dials can no longer
fully replace the peer table with peers it controls - the honest trusted
floor always survives, so the node keeps hearing the heavier canonical
chain. This is the eclipse-resistance complement to #79: #79 makes a lighter
fork unadoptable; #80-part1 keeps honest peers in the table so the heavier
chain stays reachable.

Wired at host construction over GossipSubDirectPeers (falls back to
BootstrapPeers). Test (non-short): a protected peer is reported
connmgr-protected; garbage multiaddrs are skipped.

Part 2 (head-source diversity: require >=N distinct scored peers before
adopting a head advance) is deferred as a follow-up - it needs source-peer
plumbing through blockpub->blockingest and interacts with the #79 fork
choice; not worth rushing into this release.
@Reiers Reiers changed the title v1.8.5: head catch-up + bridge-off proving + fork-choice hardening (#79 #81 #82 #83) v1.8.5: head catch-up + bridge-off proving + fork-choice & eclipse hardening (#50 #79 #80 #81 #82 #83) Jun 30, 2026
@Reiers Reiers merged commit c364dce into main Jun 30, 2026
1 check passed
@Reiers Reiers deleted the fix/83-lag-aware-gossip-skip branch June 30, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Message-block availability: StateSearchMsg fetches message blocks via bridge, stalls bridge-off

1 participant