Skip to content

fix(#118): auto-refresh stale bootstrap anchor on boot (bridge-off)#120

Merged
Reiers merged 1 commit into
mainfrom
fix/118-auto-stale-reset
Jul 11, 2026
Merged

fix(#118): auto-refresh stale bootstrap anchor on boot (bridge-off)#120
Reiers merged 1 commit into
mainfrom
fix/118-auto-stale-reset

Conversation

@Reiers

@Reiers Reiers commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Closes #118.

What

Bridge-off mode (--no-fallback-rpc) has no polling-Sync stale-reset because the polling head-source is a no-op. A daemon stopped past parentWalkCap (~24h with #117 defaults) boots wedged: the persisted anchor + header store are old, and every parent-walk from the fresh gossip head exceeds the cap. #51 solved this in the RPC path; this PR is the parallel code for bridge-off.

How

On daemon boot, if bootstrap-anchor.json mtime is older than --anchor-max-age (default 12h), re-run the same multi-source quorum probe lantern repair uses and overwrite the anchor with a fresh finality. The subsequent boot then treats this as a normal fresh-anchor start — the ChainExchange seed re-seats the header store from the fresh anchor tipset, and #117's parentWalkCap covers the remaining gap.

Additionally: if the persisted store head is more than parentWalkCap epochs behind the fresh anchor, drop the header store (chain state only, same allow-list as reset.go) and let the seed re-seat it — this covers the "store so far behind that no parent-walk can stitch it back" case.

New flags

flag default notes
`--anchor-max-age ` `12h` `0` disables the mtime check.
`--auto-stale-reset ` `true` No-op outside `--no-fallback-rpc`; RPC mode uses `--stale-reset-threshold` (#51).

Guarantees

  • No-op outside bridge-off mode (RPC path is already handled by Stale-restart auto-heal + keystore-safety (release blocker for tester announcement) #51's `StaleResetThreshold`).
  • Never touches `keystore`, `jwt-secret`, `token*`, `secrets/`, `backups/` — helper touches only `bootstrap-anchor.json` and (in the drop path) the `headerstore/` directory.
  • Quorum probe failure is fail-warn (not fail-fatal): boot continues with the stale anchor. A probe blip must never brick the daemon.
  • Write failure is fatal (partial anchor is worse than stale).
  • Cold install (no anchor file) → no-op; `lantern init` handles that case.

Tests

8 unit tests in `cmd/lantern/auto_stale_reset_test.go`:

  • `TestAutoStaleReset_TriggeredByMtime` — 24h stale + 12h max-age → probe called, write called.
  • `TestAutoStaleReset_SkippedWhenFresh` — 30 min old + 12h max-age → no probe.
  • `TestAutoStaleReset_QuorumFailureDoesNotOverwrite` — probe returns `ErrQuorumNotReached` → no write, anchor mtime unchanged, no error (fail-warn).
  • `TestAutoStaleReset_DisabledByFlag` — `enabled=false` → no-op even with a 72h-stale anchor.
  • `TestAutoStaleReset_SkippedInRPCMode` — `bridgeOff=false` → no-op (guards against double-fire with Stale-restart auto-heal + keystore-safety (release blocker for tester announcement) #51).
  • `TestAutoStaleReset_MaxAgeZeroDisables` — `maxAge=0` → no-op.
  • `TestAutoStaleReset_NoAnchorFile` — missing `bootstrap-anchor.json` → no-op.
  • `TestAutoStaleReset_WriteFailureReturnsError` — write `errors.Is` sentinel returned; anchor untouched.

Full package test suite green (`go test -count=1 -short ./...`).

Live smoke on darwin/calibration

Copied the smoke-C calibration data dir, stamped `bootstrap-anchor.json` mtime 24h into the past, booted with `--no-fallback-rpc --anchor-max-age 30m`:

```
Fetching trusted head from https://gateway.lantern.reiers.io
auto-stale-reset: bootstrap-anchor is 24h0m54s old (max-age 30m0s); re-running multi-source quorum probe before boot — chain state only, keys untouched (#118)
libp2p host: starting...
6 sources assembled (libp2p=4, forest=1, user=0, gateway=1)
✓ Quorum reached (3/3 agree): instance=411273 epoch=3881573 ...
✓ Trust anchor written: .../bootstrap-anchor.json
auto-stale-reset: fresh anchor written @ epoch 3881573 (was 24h0m54s stale; #118)
anchor: persisted quorum anchor (epoch 3881573, captured 2026-07-11T12:42:03Z) ...
seed: store head seeded from anchor @ 3881573 (2 blocks) via chainxchg peers - gateway-free, Glif-free (#91/#109)
[gossip-block] sub-rcv=6 ing-rcv=6 installed=1 backfilled=4 backfillFail=0 lastEpoch=3881579
```

  • Fresh anchor at epoch 3881573 (was 3881418, +155 epochs / ~78 min ahead of the persisted one).
  • ChainExchange seed re-seated the store from the fresh anchor.
  • `backfillFail=0`; live gossip stitched to `lastEpoch=3881579`.
  • Zero Glif hits.

Not in this PR

Depends on

Bridge-off mode (--no-fallback-rpc) has no polling Sync stale-reset
because the polling head-source is a no-op — so a daemon that was
stopped past parentWalkCap (~24h with #117 defaults) boots wedged: the
persisted anchor + header store are old and every parent-walk from the
fresh gossip head exceeds the cap. #51 solved this in the RPC path; this
change is the parallel code for bridge-off.

On daemon boot, if the persisted bootstrap-anchor.json is older than
--anchor-max-age (default 12h), re-run the same multi-source quorum
probe 'lantern repair' uses and overwrite the anchor with a fresh
finality. The subsequent boot then treats this as a normal fresh-anchor
start; the ChainExchange seed re-seats the header store from the fresh
anchor tipset, and #117's parentWalkCap covers the remaining gap.

Additionally: if the persisted store head is more than parentWalkCap
epochs behind the fresh anchor, drop the header store (chain state only,
same allow-list as reset.go) and let the seed re-seat it — covers the
'store so far behind that no parent-walk stitches it back' case.

New flags:
  --anchor-max-age <duration>  (default 12h; 0 disables)
  --auto-stale-reset <bool>    (default true; no-op outside bridge-off)

Guarantees:
- No-op outside bridge-off mode (RPC path handled by #51's
  StaleResetThreshold).
- Never touches keystore, JWT, tokens, secrets/, or backups/.
- Quorum probe failure is fail-warn (not fail-fatal): boot continues
  with the stale anchor; a probe blip must never brick the daemon.
- Write failure is fatal (partial anchor is worse than stale).

Tests: 8 unit tests covering triggered/skipped/probe-failure/
write-failure/disabled/RPC-mode/max-age-zero/no-anchor-file. Helper is
factored so the probe + writer are injectable — no live network needed.

Live smoke on darwin/calibration: 24h-stale anchor triggered the probe
(3/3 quorum in 328ms), fresh anchor written at epoch 3881573 (was
3881418, +155 epochs stale), ChainExchange seed re-seated the store,
gossip caught up with backfillFail=0 and zero Glif hits.

Fixes #118.
@Reiers Reiers merged commit 8833f05 into main Jul 11, 2026
1 check passed
@Reiers Reiers deleted the fix/118-auto-stale-reset branch July 11, 2026 12:48
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.

bridge-off boot: auto-reset stale anchor + headerstore when persisted state is beyond parentWalkCap (#51 gap)

1 participant