fix(#118): auto-refresh stale bootstrap anchor on boot (bridge-off)#120
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 pastparentWalkCap(~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.jsonmtime is older than--anchor-max-age(default 12h), re-run the same multi-source quorum probelantern repairuses 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'sparentWalkCapcovers the remaining gap.Additionally: if the persisted store head is more than
parentWalkCapepochs behind the fresh anchor, drop the header store (chain state only, same allow-list asreset.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
Guarantees
Tests
8 unit tests in `cmd/lantern/auto_stale_reset_test.go`:
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
```
Not in this PR
Depends on