From e740e95160b806b5618c262ea65645cb71de534d Mon Sep 17 00:00:00 2001 From: Nicklas Reiersen Date: Sun, 12 Jul 2026 14:08:39 +0200 Subject: [PATCH] fix(#123): --devnet-head-poll-interval knob + auto-match block cadence Closes lantern#123 findings 8+9. On a single-node docker devnet the gossipsub mesh can't form, so ChainHead + eth_blockNumber arrive ONLY via the RPC-fallback poll. The mainnet-oriented 30s cadence set by startGossipHead (and its cmd/lantern twin) is 7.5x block-time on a 4s-epoch devnet, so head trails live by ~80 epochs = ~320s at all times \u2014 breaks any timing-sensitive consumer (curio schedulers, proving-window logic, latency-sensitive read-your-write patterns). On devnet, the daemon now derives the effective poll interval from build.GetDevnetConfig().BlockDelaySecs (captured at devnet-init time by #125), falling back to 4s if the config is absent or older than #125. Operators can override with --devnet-head-poll-interval (CLI) or Config.DevnetHeadPollInterval (embedded). Mainnet + calibration paths are untouched: the gossip-primary logic (30s catch-up cadence when libp2p is enabled) still runs; only when network == build.Devnet does the derivation fire. Applied to both consumer paths: - cmd/lantern/main.go (standalone `lantern daemon` CLI) - pkg/daemon/start.go (embedded consumers like curio-core) Live smoke, curio-fork docker devnet at height 17798+: Cold-boot head lag (default, sync every 4s): t=10s lotus=17798 lantern=17794 lag= 4 epochs t=30s lotus=17803 lantern=17796 lag= 7 epochs t=60s lotus=17810 lantern=17803 lag= 7 epochs With --devnet-head-poll-interval 2s (user override honored): Boot log: "header store: ... (sync every 2s, buf=64)" Pre-fix baseline (from #123 finding 8+9): ~80 epochs sustained. Full suite: go test ./... reports 52 packages ok, no failures. --- CHANGELOG.md | 24 +++++++++++++++++++++--- cmd/lantern/main.go | 19 +++++++++++++++++++ pkg/daemon/daemon.go | 11 +++++++++++ pkg/daemon/start.go | 18 ++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7da40a..4068a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,9 +117,27 @@ are byte-identical to pre-change behaviour. hinting `lantern devnet-init --force` to re-capture from the running lotus. -Not in this PR: `ChainHead` head-poll lag on devnet ([#123](https://github.com/Reiers/lantern/issues/123) -findings 8+9). Folding it into a separate `--devnet-head-poll-interval` -knob PR. +Not in this PR: standalone `cmd/lantern` mpool wiring baseline (a +pre-existing gap discovered during the live-smoke run for this PR). + +### Devnet: `--devnet-head-poll-interval` knob (closes [#123](https://github.com/Reiers/lantern/issues/123) findings 8+9) + +- **Devnet header-store poll interval now defaults to match block + cadence instead of the mainnet 30s.** On a single-node docker devnet + the gossipsub mesh can't form, so head arrives ONLY via RPC polling; + the mainnet-oriented 30s cadence lags a 4s-epoch devnet by ~7x block + time (finding 8+9 originally documented lags of ~80 epochs = ~320s). + On devnet, the daemon now derives the poll interval from + `build.GetDevnetConfig().BlockDelaySecs` (captured at devnet-init time + by [#125](https://github.com/Reiers/lantern/pull/125)), falling back to 4s if unavailable. + Live smoke: cold-boot head lag dropped from ~80 epochs to 4-7 epochs + sustained (~10x improvement). + +- **New `--devnet-head-poll-interval` flag** (cmd/lantern) + + `Config.DevnetHeadPollInterval` field (pkg/daemon) let operators + override the derived cadence. Zero (default) means auto: read from + `BlockDelaySecs`. Ignored on mainnet / calibration (the gossip-primary + cadence logic stays untouched there). ### Devnet: bridge-first read ordering for eth_* (closes [#123](https://github.com/Reiers/lantern/issues/123) finding 7) diff --git a/cmd/lantern/main.go b/cmd/lantern/main.go index 176396f..88107d4 100644 --- a/cmd/lantern/main.go +++ b/cmd/lantern/main.go @@ -736,6 +736,7 @@ func cmdDaemon(args []string) error { noHS := fs.Bool("no-header-store", false, "Disable the persistent header store (legacy synthetic-head mode)") hsPath := fs.String("header-store", "", "Header store BadgerDB path (default: //headerstore)") syncInterval := fs.Duration("sync-interval", 6*time.Second, "Header-store sync poll interval") + devnetHeadPollInterval := fs.Duration("devnet-head-poll-interval", 0, "Devnet-only: override the header-store poll interval on devnet (default 0 = auto: use BlockDelaySecs from devnet-config, or 4s if unavailable). Fixes lantern#123 findings 8+9: a single-node docker devnet can't form a gossipsub mesh, so head updates come only from RPC polling; the mainnet-oriented 30s cadence lags a 4s-cadence devnet by ~7x block time. Ignored on mainnet/calibration.") staleResetThreshold := fs.Int64("stale-reset-threshold", 2880, "Epochs behind live head past which a persisted header store re-anchors near live head instead of backfilling (#51). 0 disables. Chain state only; keys are never touched.") notifyBufSize := fs.Int("notify-buf", headnotify.DefaultBufferSize, "ChainNotify per-subscriber buffer size") p2pListen := fs.String("p2p-listen", "/ip4/0.0.0.0/tcp/0,/ip4/0.0.0.0/udp/0/quic-v1", "libp2p listen multiaddrs (comma-separated). Empty disables the libp2p host.") @@ -1175,6 +1176,24 @@ func cmdDaemon(args []string) error { if gossipEnabled && effSyncInterval == 6*time.Second { effSyncInterval = 30 * time.Second } + // lantern#123 findings 8+9: on devnet, a single-node docker cluster + // can't form a gossipsub mesh, so head arrives ONLY via polling. The + // mainnet-oriented 30s cadence set above is 7.5x block-time on a + // 4s-epoch devnet. Override with the user's --devnet-head-poll-interval + // flag if set, otherwise derive from the devnet-config BlockDelaySecs + // (captured at devnet-init time by #125), falling back to 4s. + if network == build.Devnet { + switch { + case *devnetHeadPollInterval > 0: + effSyncInterval = *devnetHeadPollInterval + default: + cadence := 4 * time.Second + if devnetCfg := build.GetDevnetConfig(); devnetCfg != nil && devnetCfg.BlockDelaySecs > 0 { + cadence = time.Duration(devnetCfg.BlockDelaySecs) * time.Second + } + effSyncInterval = cadence + } + } sync = hstore.NewSync(store, src, hstore.SyncOptions{ Interval: effSyncInterval, MaxBacktrack: 60, diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index 5f58c78..ee251bc 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -100,6 +100,17 @@ type Config struct { // SyncInterval is the header-store poll cadence. Default 6s. SyncInterval time.Duration + // DevnetHeadPollInterval, when > 0, overrides the header-store poll + // cadence on devnet only. Zero (default) means auto: use + // build.GetDevnetConfig().BlockDelaySecs (persisted by + // `lantern devnet-init`), or 4s if unavailable. Fixes lantern#123 + // findings 8+9: a single-node docker devnet can't form a gossipsub + // mesh, so head arrives ONLY via RPC polling; the mainnet-oriented + // 30s cadence lags a 4s-epoch devnet by ~7x block time. Ignored on + // mainnet + calibration (SyncInterval / gossip logic is unchanged + // there). + DevnetHeadPollInterval time.Duration + // PersistentCache enables the on-disk (Badger) block cache instead of // the in-memory MemBlockStore. This is the PDP / mid-node tier: the warm // contract subtrees (PDP/payments/registry/USDFC) SURVIVE restart, so a diff --git a/pkg/daemon/start.go b/pkg/daemon/start.go index 4c600df..e593b48 100644 --- a/pkg/daemon/start.go +++ b/pkg/daemon/start.go @@ -226,6 +226,24 @@ func (d *Daemon) startInternal(ctx context.Context) error { if libp2pEnabled { syncInterval = 30 * time.Second } + // lantern#123 findings 8+9: on devnet, a single-node docker cluster + // can't form a gossipsub mesh, so head arrives ONLY via polling. The + // mainnet-oriented 30s cadence set above is 7.5x block-time on a 4s + // devnet. Honor an explicit DevnetHeadPollInterval override, otherwise + // derive from the devnet-config BlockDelaySecs (captured at + // devnet-init time by #125), falling back to 4s. + if network == build.Devnet { + switch { + case d.cfg.DevnetHeadPollInterval > 0: + syncInterval = d.cfg.DevnetHeadPollInterval + default: + cadence := 4 * time.Second + if devnetCfg := build.GetDevnetConfig(); devnetCfg != nil && devnetCfg.BlockDelaySecs > 0 { + cadence = time.Duration(devnetCfg.BlockDelaySecs) * time.Second + } + syncInterval = cadence + } + } // Issue #33: the hardened Sync resumes catch-up contiguously from // currentHead+1 in CatchUpChunk-sized steps (never skips epochs),