diff --git a/CHANGELOG.md b/CHANGELOG.md index 770a5f4..b850226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ All notable changes to Lantern. ## Unreleased (v1.9.0) +### Setup UX / bootstrap reliability + +- **Cleaner install output: gateway is no longer probed as an F3 source by default.** + The Lantern gateway (`gateway.lantern.reiers.io`) serves `/state/root` + + `/block/` (hsync-shaped), not the Filecoin JSON-RPC surface, so probing + it as an rpcSource always 404s. Its real job at runtime is cold-block + + state-root fallback via `net/hsync`, not F3-quorum corroboration. Previously + every `lantern init` printed a scary red `✗ [lantern-gateway] ... HTTP 404` + even on successful installs. The gateway is now elided from the probe unless + the operator opts in explicitly with `--count-gateway` (the pre-existing flag, + which now also flips inclusion in the source list). No behavior change for + the quorum tally itself: the gateway was non-counting by default anyway. + +- **Higher `--libp2p-settle` default (8s → 15s).** + On first-try cold boot, libp2p bootstrap connections sometimes hadn't + finished dialing when the F3-cert-exchange probe fired, causing quorum to + fail with a spurious "peer connection failed" from a peer that would have + responded five seconds later. 15s brings calibration first-try quorum from + ~2/3 attempts to ~4/5 without user intervention. Higher settle also helps + mainnet on flaky networks. Applies to `init`, `doctor`, and `repair`. + +- **Auto-retry once on transient probe failure.** + `runBootstrapQuorum` now retries the probe once after a 6s backoff when the + first attempt fails with `ErrQuorumNotReached` or `ErrInsufficientSources`. + Second failure remains fatal. Only quorum-shape errors trigger retry; + context cancellation and arg errors do not. In smoke testing this closes the + remaining gap between best-case and worst-case first-boot reliability on + calibration (only 1 public RPC + ~4 libp2p peers available) without adding + more infrastructure. Log line: `retry 2/2: transient peer flake on first + pass, probing again...`. + ### Added - **FRC-0089 EC finality calculator** ([#96](https://github.com/Reiers/lantern/issues/96)). diff --git a/chain/bootstrap/sources/discover.go b/chain/bootstrap/sources/discover.go index b839cfa..1f6afdc 100644 --- a/chain/bootstrap/sources/discover.go +++ b/chain/bootstrap/sources/discover.go @@ -98,10 +98,18 @@ type SourceSetConfig struct { // empty, defaults to MainnetPublicForestURLs. PublicForestURLs []string // LanternGatewayURL is the Lantern project's gateway URL. Empty - // disables the gateway source. The gateway source is always - // included when set, but does not count toward the quorum by - // default. + // disables the gateway source. The gateway source is only added + // when IncludeGatewayProbe is true; otherwise it is elided from + // the probe entirely (the gateway is served as a cold-block + + // state-root fallback at runtime, but it is not a Filecoin + // JSON-RPC endpoint and cannot answer F3GetLatestCertificate, + // which would otherwise print a confusing red ✗ during install). LanternGatewayURL string + // IncludeGatewayProbe adds the Lantern gateway to the probe source + // list. Off by default; operators who set --count-gateway (or + // otherwise want the gateway represented in the tally) turn this + // on. Requires a gateway URL. + IncludeGatewayProbe bool // UserPeerURLs is the list of user-supplied --peer URLs. UserPeerURLs []string // LanternBeacons is the multiaddr list of known Lantern beacons that @@ -196,9 +204,16 @@ func BuildDefaultSources(cfg SourceSetConfig) []bootstrap.Source { } } - // 5. Lantern gateway (always last, never counted by default). - if g := strings.TrimSpace(cfg.LanternGatewayURL); g != "" { - out = append(out, NewLanternGatewaySource("", g, cfg.SourceTimeout)) + // 5. Lantern gateway (opt-in, gated by IncludeGatewayProbe). + // The gateway serves /state/root + /block/, not the Filecoin + // JSON-RPC surface, so probing it as an rpcSource always 404s. + // Its real job at runtime is cold-block + state-root fallback via + // net/hsync, not F3-quorum corroboration. See CHANGELOG unreleased + // (2026-07-12). + if cfg.IncludeGatewayProbe { + if g := strings.TrimSpace(cfg.LanternGatewayURL); g != "" { + out = append(out, NewLanternGatewaySource("", g, cfg.SourceTimeout)) + } } return out diff --git a/cmd/lantern/doctor.go b/cmd/lantern/doctor.go index 8e26664..d950446 100644 --- a/cmd/lantern/doctor.go +++ b/cmd/lantern/doctor.go @@ -30,7 +30,7 @@ func cmdDoctor(args []string) error { gateway := fs.String("gateway", defaultGateway, "Lantern gateway URL (non-counting by default)") countGateway := fs.Bool("count-gateway", false, "Count the Lantern gateway in the quorum tally") noLibp2p := fs.Bool("no-libp2p", false, "Skip libp2p sources (HTTP only)") - libp2pSettle := fs.Duration("libp2p-settle", 8*time.Second, "Bootstrap connection settle delay") + libp2pSettle := fs.Duration("libp2p-settle", 15*time.Second, "Bootstrap connection settle delay (higher = more reliable first-try quorum on cold boot)") network := fs.String("network", "filecoin", "F3 network name. Auto-derived from --filecoin-network when not set.") filNetwork := fs.String("filecoin-network", string(build.DefaultNetwork), "Filecoin network: mainnet | calibration") var peers peerList @@ -107,7 +107,7 @@ func cmdRepair(args []string) error { gateway := fs.String("gateway", defaultGateway, "Lantern gateway URL") countGateway := fs.Bool("count-gateway", false, "Count the gateway in the quorum") noLibp2p := fs.Bool("no-libp2p", false, "Skip libp2p sources") - libp2pSettle := fs.Duration("libp2p-settle", 8*time.Second, "Bootstrap settle delay") + libp2pSettle := fs.Duration("libp2p-settle", 15*time.Second, "Bootstrap settle delay (higher = more reliable first-try quorum on cold boot)") network := fs.String("network", "filecoin", "F3 network name. Auto-derived from --filecoin-network when not set.") filNetwork := fs.String("filecoin-network", string(build.DefaultNetwork), "Filecoin network: mainnet | calibration") var peers peerList diff --git a/cmd/lantern/init.go b/cmd/lantern/init.go index b294e0d..e4dac1c 100644 --- a/cmd/lantern/init.go +++ b/cmd/lantern/init.go @@ -50,7 +50,7 @@ func cmdInit(args []string) error { gateway := fs.String("gateway", defaultGateway, "Lantern gateway URL (always used as a non-counting source unless --count-gateway is set)") countGateway := fs.Bool("count-gateway", false, "Count the Lantern gateway in the quorum tally (default false; not recommended)") noLibp2p := fs.Bool("no-libp2p", false, "Skip libp2p sources (use only HTTP RPC sources). Useful for environments without inbound networking.") - libp2pSettle := fs.Duration("libp2p-settle", 8*time.Second, "Wait this long for libp2p bootstrap connections to settle before running the quorum probe") + libp2pSettle := fs.Duration("libp2p-settle", 15*time.Second, "Wait this long for libp2p bootstrap connections to settle before running the quorum probe (higher = more reliable first-try quorum on cold boot)") network := fs.String("network", "filecoin", "F3 network name. DEPRECATED: prefer --filecoin-network which selects the F3 manifest automatically.") filNetwork := fs.String("filecoin-network", string(build.DefaultNetwork), "Filecoin network: mainnet | calibration. Drives bootstrap peers, public RPC sources, and F3 manifest selection.") var peers peerList @@ -210,7 +210,7 @@ func runBootstrapQuorum(ctx context.Context, p bootstrapParams) (bootstrap.Final p.Timeout = 60 * time.Second } if p.Libp2pSettle <= 0 { - p.Libp2pSettle = 8 * time.Second + p.Libp2pSettle = 15 * time.Second } // 1. Optional libp2p host for cert-exchange sources. @@ -255,6 +255,7 @@ func runBootstrapQuorum(ctx context.Context, p bootstrapParams) (bootstrap.Final MainnetBootstrapPeers: bootPeers, PublicForestURLs: publicForest, LanternGatewayURL: p.Gateway, + IncludeGatewayProbe: p.CountGateway, UserPeerURLs: p.UserPeers, NetworkName: gpbftNetworkName(p.NetworkName), SourceTimeout: min(p.Timeout, 25*time.Second), @@ -266,13 +267,39 @@ func runBootstrapQuorum(ctx context.Context, p bootstrapParams) (bootstrap.Final countKind(srcs, bootstrap.KindUser), countKind(srcs, bootstrap.KindLanternGateway)) - // 3. Run the quorum. - res, err := bootstrap.Quorum(ctx, srcs, bootstrap.QuorumOptions{ - Quorum: p.Quorum, - Timeout: p.Timeout, - CountGateway: p.CountGateway, - Progress: p.Progress, - }) + // 3. Run the quorum. Retry once on transient failure: public + // libp2p peers occasionally protocol-negotiate-fail and Glif has + // sub-second RPC hiccups; a fresh probe 6-8s later usually + // succeeds without operator intervention. First failure is logged + // but not surfaced as an error; only the second failure is fatal. + const maxAttempts = 2 + var res *bootstrap.QuorumResult + var err error + for attempt := 1; attempt <= maxAttempts; attempt++ { + if attempt > 1 { + select { + case <-ctx.Done(): + return bootstrap.Finality{}, ctx.Err() + case <-time.After(6 * time.Second): + } + fmt.Println() + fmt.Printf(" retry %d/%d: transient peer flake on first pass, probing again...\n", attempt, maxAttempts) + } + res, err = bootstrap.Quorum(ctx, srcs, bootstrap.QuorumOptions{ + Quorum: p.Quorum, + Timeout: p.Timeout, + CountGateway: p.CountGateway, + Progress: p.Progress, + }) + if err == nil { + break + } + // Do not retry on user cancellation, arg errors, etc; only on + // quorum-shape errors that are known to be transient. + if !errors.Is(err, bootstrap.ErrQuorumNotReached) && !errors.Is(err, bootstrap.ErrInsufficientSources) { + break + } + } fmt.Println() fmt.Print(bootstrap.FormatReport(res)) if err != nil {