Skip to content

Three-tier node (Light/PDP/Full) + head-reliability hardening#95

Merged
Reiers merged 15 commits into
mainfrom
fix/lightnode-messages-2026-07-01
Jul 1, 2026
Merged

Three-tier node (Light/PDP/Full) + head-reliability hardening#95
Reiers merged 15 commits into
mainfrom
fix/lightnode-messages-2026-07-01

Conversation

@Reiers

@Reiers Reiers commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

Turns Lantern from a single light client into a three-tier node (Light / PDP / Full), chosen at install time, and lands the head-reliability + validation hardening needed before the next release.

Install-time node tiers

  • install.sh 3-way selector (Light / PDP / Full); pkg/nodeprofile persists the choice per-network; lantern node-type command; daemon reads it at start. Light stays light — only the picked tier provisions the larger cache/footprint.

PDP / mid tier (F1)

  • Persistent bounded Badger block cache (state/cache, restart-warm) + warm-set pinning of static contract subtrees + /fil/blocks block-submit for a backup producer.

Full-node foundation (epic #87)

  • chain/fullvalidate: pure-Go per-block consensus validation (signature, election + ticket VRF, miner eligibility, win-count) — the Lotus filcns set minus the two ffi calls, which stay F3-trusted. Tier-gated, observe-by-default, wired into ingest.
  • proofs/winningpost: scaffold for the pure-Go WinningPoSt SNARK verify (Stage B, Stage B: pure-Go WinningPoSt verify (remove filecoin-ffi dep #1) #88 — stub, not a working verifier yet).

Head reliability / eclipse hardening

  • Heaviest-ParentWeight fork choice + head-adoption divergence gate (no-adopt while the running head diverges from the independent-source quorum) + Kind-diverse periodic head corroboration.

Docs / hygiene

  • README rewritten for the three-tier model + full-node roadmap. Private references scrubbed from source comments.

Verification

Full module builds CGO_ENABLED=0 (no CGo / no Rust); vet + gofmt clean; test suites green. (pkg/daemon TestStartAnchorsAgainstNetwork needs port :1234 free — passes when no local daemon holds it.)

Issue status

Closes #79
Closes #85

Advances (kept open, pending live calibration soak / further work): #87 #88 #90 #94 #76.

Supersedes #86 (its commit is included here).

Reiers added 15 commits July 1, 2026 01:09
…e monitor

#85 item 1 (header propagation): tighten the block gossipsub TopicValidator
so Lantern only ValidationAccepts - and therefore only re-propagates - blocks
that fully consume the payload (no trailing bytes) and whose header round-trips
through canonical CBOR to its declared CID. Propagation and ingestion now share
one integrity bar; Lantern can't be turned into an amplifier for a malformed or
fake-CID header.

#85 item 2 / #80 part 2 (robust head discovery): new chain/headcheck package -
a periodic running-head divergence monitor. It asks a diversity of INDEPENDENT
head sources (counted by source Kind, so N URLs of one upstream count once) what
epoch the head is at and checks agreement within a 3-block lookback. On a quorum
of independent sources diverging from our gossip head it raises an eclipse/fork
alarm. It never moves our head (gossip + #79 fork choice do that) and never makes
a trusted RPC the head oracle - it only corroborates or disputes.

Wired best-effort into pkg/daemon (Config.HeadCheckRPCs); disabled when empty so
it can't sink head-tracking. TRUST-MODEL.md threat-model row updated to reflect
the stacked tip defenses (#79 weight fork choice, #80 peer floor, #85 propagation
gate + divergence monitor; F3 closes it on mainnet).

Tests: chain/headcheck full suite (agreement, kind-diversity collapse, divergence
alarm, insufficient-sources, median, start/stop); blockpub validator trailing-
garbage + nil-header rejection. Full suite 44 pkgs green (-short, LANTERN_OFFLINE=1),
build+vet+gofmt clean.
Light-node hardening pass so all message/RPC paths work bridge-off and an
embedded node self-heals from downtime.

- pkg/daemon: wire StaleResetThreshold auto-heal (#51) into the EMBEDDED
  path (was only in standalone cmd/lantern). Config.StaleResetThreshold
  knob (0=default 2880 epochs ~1d, <0=disabled) + OnStaleReset log.
  Fixes the wedge that forced embedded testers to manually
  'lantern reset --chain-state' after downtime > MaxBacktrack.
  Extracted resolveStaleResetThreshold + regression tests.
- eth_sendRawTransaction: fail LOUD with an actionable error when the
  local send path is unavailable (gossipsub mpool not wired) and no
  bridge is set, instead of the misleading generic
  'FEVM method requires --vm-bridge-rpc'. (beck's AddPiece error root.)
- eth_feeHistory: NEW local-first path served from header-store
  ParentBaseFee (last read method on the SP write path that hard-failed
  bridge-off). Bridge stays as out-of-window fallback. +parse/fallback tests.
- eth_subscribe(logs): allow bridge-off operation with a header store
  (EthGetLogs is local-first since #73); refuse only when neither a
  header store nor a bridge exists. Corrected stale comment + test.

Full suite green (44 pkgs, -short LANTERN_OFFLINE=1), build+vet+gofmt clean.
…blockpub gate) into lightnode fixes

Combines the light-node bridge-off/auto-heal work with snadrus#85 (PR #86):
- chain/headcheck: running-head divergence monitor (corroborate-don't-oracle,
  Kind-diversity counting, 3-block lookback) wired best-effort into pkg/daemon
  via Config.HeadCheckRPCs (disabled when empty).
- net/blockpub: header-propagation integrity gate (full-payload consume +
  canonical-CBOR CID re-derive; can't be an amplifier for a malformed/fake CID).
- TRUST-MODEL.md: gossipsub-eclipse threat row updated for stacked tip defenses.
Clean 3-way auto-merge (StaleReset + headcheck touch disjoint regions).
Full suite green (build+vet+gofmt), 0 failures.
…ble status (#79/#80)

Close the deferred halves of #79 (periodic re-quorum) / #80 (head-source
diversity) on top of the #86 divergence monitor:

- chain/headcheck: NEW GatewayHeadSource (HTTP /state/root) so the Lantern
  gateway is an independent-KIND corroborating head source (KindLanternGateway),
  not just operator RPC URLs. +adapter tests.
- pkg/daemon: build a Kind-DIVERSE headcheck source set automatically -
  operator HeadCheckRPCs (Forest) + gateway (LanternGateway) + fallback/Glif
  (Forest) - so the running head is continuously corroborated by a multi-kind
  quorum (counts agreement by Kind, N URLs of one kind = 1). Monitor now starts
  whenever >=1 corroborating source exists; self-reports StatusInsufficient
  until enough distinct kinds are reachable (safe to enable broadly).
- Daemon.HeadCorroboration(): expose the monitor's latest verdict (agree/
  diverge/insufficient + local vs median-external head + tallies) so an eclipse
  alarm is VISIBLE (dashboard/health/RPC), not just a log line. Status-only:
  never auto-halts (a false positive must not self-DoS); the head still advances
  on gossip + #79 heaviest-weight fork choice.

Full suite green (build+vet+gofmt), 0 failures.
…DP tier

The light node runs cache-first on an in-memory MemBlockStore, so every
restart is cold: a restarted node re-fetches its whole warm set (PDP/
payments/registry/USDFC contract subtrees) from the gateway on the first
head advance. Fine for a wallet; not fine for a PDP node that must prove/
settle against warm contract state on a schedule and cannot stall inside a
proving window.

state/cache: NEW persistent, bounded, CID-keyed block cache backed by
Badger (already a dep via the header store). The warm set SURVIVES restart.
- CID-keyed exactly like MemBlockStore: content-addressed trust unchanged;
  PutVerify re-hashes untrusted bytes and rejects mismatch (no cache poison).
- Bounded: soft byte budget (default 3 GiB = middle of the 2-5 GB PDP tier)
  with sampled-LRU eviction over UNPINNED blocks; pinned warm-set subtrees
  are never evicted (hard floor).
- Live-bytes counter persisted + recomputed on Open so the cap holds across
  restarts. Single-flight background eviction + blocking evictNow for the
  sync path/tests.
- Satisfies combined.Cache (drop-in for MemBlockStore). Iface assertion
  lives in the test pkg to avoid a state/cache->net/combined import cycle.

pkg/daemon: Config.PersistentCache / PersistentCacheBytes opt-in. When set,
the daemon opens the cache at <DataDir>/<network>/blockcache, wires it as
the fetcher cache, closes it in stopInternal, and exposes BlockCacheStats().
Light tier leaves it false and stays memory-cached.

Tests (race-clean): put/get/has, PutVerify mismatch rejection, persistence
across reopen (the PDP guarantee), LRU eviction respecting soft cap + pins.
Full suite green (build+vet+gofmt), 0 failures.
The persistent cache survives restart, but without pinning it's just
persistent - LRU could still evict the exact PDP/payments/registry/USDFC
contract state a PDP node proves/settles against. This closes the loop:

- state/kamt: WalkSubtree gains an optional OnNode(cid) callback, fired
  once per fetched+verified node (root included). +tests (visits-every-node,
  nil-noop).
- state/prefetch: Prefetcher gains an optional Pinner (Pin(cid) error,
  satisfied by *state/cache.Store) + SetPinner. On a STATIC (configured)
  contract walk it pins the storage root, bytecode CID, and every walked
  storage-trie node. DYNAMIC (client-learned, eth_call-miss) addresses are
  deliberately NOT pinned - that set is attacker-influenceable and must stay
  evictable, or a hostile client could spray addresses to pin-flood the cache.
- pkg/daemon: when PersistentCache is on, SetPinner(blockCache) on the
  prefetcher so the warm set persists un-evicted across restart.

Net: a restarted PDP node comes up with its contract warm set already
resident (no cold gateway re-fetch inside a proving window), and LRU only
reclaims incidental/dynamic blocks. Full suite green, 0 failures.
…de block submit)

The block-production surface (MinerGetBaseInfo -> MinerCreateBlock -> sign
-> SyncSubmitBlock -> blockpub /fil/blocks) already existed and is gated on
AllowBlockSubmit + a VM bridge (needed for a valid post-execution state root;
the no-CGo wall). But the embedded daemon DISCARDED the *blockpub.Publisher
that blockingest.Start returns (`ing, _, err :=`), so an embedded PDP/backup
node could create a block but had no way to publish it - SyncSubmitBlock's
c.Mpool.(BlockPublisher) assert always failed because net/mpool publishes on
/fil/msgs, not /fil/blocks.

- pkg/daemon: capture the block publisher from blockingest.Start, store it
  (d.blockPub), and wire it onto the ChainAPI via SetBlockPublisher. Cleared
  on stop. Unconditional wiring; SyncSubmitBlock still gates on AllowBlockSubmit.
- rpc/handlers: ChainAPI gains an explicit blockPub field + SetBlockPublisher.
  SyncSubmitBlock now PREFERS the explicit /fil/blocks publisher, falls back to
  a block-capable Mpool, and returns a clear error (not a nil-deref) when none
  is wired. BlockPublisher doc corrected (it's net/blockpub, not an mpool ext).
- Tests: gate-off no-op, no-publisher error, explicit-publisher happy path,
  publish-error propagation.

Net: a backup-tier daemon (AllowBlockSubmit + VM bridge) can now actually
submit blocks to the network - the last missing hop for the PDP node to
double as a backup block producer. Full suite green, 0 failures.
…time flag

Nicklas's correction: the node CLASS must be chosen at INSTALL time, not via
a runtime flag on one universal binary - otherwise a wallet-only light node
still carries the PDP footprint. The installer asks, and only the chosen tier
provisions the larger footprint.

- pkg/nodeprofile: persist the tier at <home>/<network>/node-profile.json
  (atomic write). Light => in-memory cache (small). PDP/Full => persistent
  2-5 GB cache + block-submit opt-in. Missing file => Light (safe default for
  pre-tier installs); malformed => error (never silently downgrade a PDP node).
  Per-network + forward-compatible. +tests.
- cmd/lantern daemon: load the profile at startup and pick the cache tier from
  it - MemBlockStore for light, persistent state/cache (survives restart) for
  PDP/Full, with the tier's cache budget. Profile AllowBlockSubmit is honored
  (still requires --vm-bridge-rpc). Also wired the previously-discarded
  /fil/blocks publisher into the CLI ChainAPI so PDP can actually submit blocks.
- cmd/lantern node-type: new subcommand to read/set the tier
  (`lantern node-type pdp --cache-gb 5 --allow-block-submit`). Fixed a
  flag-ordering bug (tier positional pulled off the front before flag parse;
  smoke test caught --network/--cache-gb being dropped when placed after the
  tier arg).
- install.sh: 3-way node-type selector (Light default / PDP / Full) that
  persists the choice via `lantern node-type`. Non-interactive via
  LANTERN_NODE_TYPE + LANTERN_PDP_CACHE_GB + LANTERN_ALLOW_BLOCK_SUBMIT.

Smoke-tested live: set pdp calibration 5GB+submit -> correct profile written;
mainnet stays light; per-network independent. Full suite green, 0 failures.
…ming

Clearer 3-way selector wording (Light Node / PDP Node / Full Node), Light
described as clients/wallets/backup, Full framed as snapshot-free full node
(track in progress). No behavior change.
Stage B groundwork to remove filecoin-ffi (Rust) proof-verify dep. Reuses
go-state-types/proof types; Verify() is an honest ErrNotImplemented stub with
the port checklist (challenge derivation, public inputs, Groth16/BLS12-381
pairing, vk load). Green test + CGO-off build. No real verify yet.
…idate)

Second-pass consensus checks a Full node can run with resident F3-anchored
state: block signature (worker key), election + ticket VRF, win-count recompute
vs parent QA-power. Byte-for-byte mirrors Lotus filcns MINUS the two ffi calls
(WinningPoSt SNARK #88, FVM re-exec #89), which stay F3-trusted. Result flags
WinningPoStVerified/StateReExecuted stay false (test-pinned) so the trust
boundary can't silently drift. ChainAPI.FullValidateView() adapts existing
StateMinerInfo/StateAccountKey/StateMinerPower into the StateView. Pure-Go,
CGO-off build+vet+gofmt+tests green.
…-by-default)

Sync.SetBlockValidator(fn, fatal) runs the chain/fullvalidate consensus check
per ingested block after the shape check. nil on Light/PDP (zero cost); the
Full tier wires it via profile.FullValidation(). Observe mode logs without
rejecting; fatal mode turns a bad block into a fetch hole so it never becomes
head (matches the #33 non-stall design). Config.FullValidation[Fatal] exposes
the same to embedded consumers. Both CLI + embedded daemon paths wired.

Tests: observe mode advances head despite always-fail validator; fatal mode
refuses to advance head to a rejected block. Full module CGO-off green.

(Note: pkg/daemon TestStartAnchorsAgainstNetwork needs :1234 free; unrelated
flake when a live lantern daemon holds the port.)
…plete)

- StateView.MinerEligible mirrors Lotus MinerEligibleToMine (post-v3): non-empty
  QA-power claim + no fee debt + no active consensus fault. Adapter reads power
  claim, miner FeeDebt(), and ConsensusFaultElapsed vs head. Rejected block from
  an ineligible miner now fails validation (Result.EligibilityOK).
- Store.LatestBeaconEntry walks a block's own entries then parents (bounded 20)
  to supply prevBeacon, so beacon-entry-less blocks validate fully instead of
  observe-only. Both CLI + embedded closures now pass a resolved prevBeacon.
- Tests + mock updated. Full module CGO-off build/vet/gofmt/tests green.
Closes the last #79 acceptance gap. Item 1 (heaviest-ParentWeight fork choice)
already shipped; this adds item 2's 'no-adopt on disagreement':

- Ingestor.SetHeadAdoptionGate(fn) gates head adoption. While headcheck reports
  the running head DIVERGES from the independent-source quorum, the gate closes
  and the ingestor HOLDS head (still receives + backfills, just won't walk head
  onto an uncorroborated tip). Reopens on re-corroboration. nil = always adopt
  (Light/PDP / no sources). New HeldDiverged stat.
- Daemon wires the gate to the headcheck monitor: StatusDiverge closes it (loud
  warn), StatusAgree reopens it, StatusInsufficient leaves it OPEN (never freeze
  a lightly-corroborated node).

Tests: gate-closed holds a valid heavier head; reopened adopts; nil always
adopts; existing fork-choice tests still green. Full module CGO-off green.
- README: Lantern is now Light / PDP / Full (chosen at install time). New tier
  table, tier section, install prompt, audience rows, full-node roadmap pointer
  (epic #87). Badge -> v1.9.0-rc1.
- Scrub internal/private references from source comments + changelog: drop
  private-thread + individual-handle attributions in favor of neutral public
  issue numbers. No code behavior change.
@Reiers Reiers merged commit da8a88d into main Jul 1, 2026
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.

Chain-Head Reliability Running-head fork choice: heaviest-ParentWeight + periodic re-quorum (not boot-only)

1 participant