feat(devnet): first-class support for local Curio devnet#122
Merged
Conversation
A locally-hosted Curio docker devnet (curio-fork/docker/, `make devnet/up`) runs at 4s block time with FWSS + PDP + Multicall3 pre-deployed. That's a much faster loop for exercising the full Lantern + Curio Core stack than calibration (30s epochs, external dependency) or mainnet (real FIL). The blocker was that Lantern hardcoded Mainnet + Calibration as the only build.Network variants: no bootstrap peers, no gossip topics, no genesis CID, no wire-name mapping for a devnet. ## What's new **build/devnet.go** — new file with: - `Devnet Network = "devnet"` constant. - `DevnetConfig` struct capturing the per-boot devnet identity. - `ConfigureDevnet(cfg)` runtime setter + `GetDevnetConfig()` accessor + `IsDevnetConfigured()` predicate. Called by the CLI before any Network method dispatch. - `LoadDevnetConfig(path)` / `SaveDevnetConfig(path, cfg)` for the on-disk JSON at `<data-dir>/devnet/devnet-config.json` (atomic tmp+rename write). - Every Network method dispatch panics-with-help when Devnet is used before ConfigureDevnet — pointing operators at `lantern devnet-init`. **build/network.go** — every getter switch (`NetworkName`, `GenesisCID`, `GossipTopicBlocks/Messages`, `BootstrapPeers`, `F3Manifest`, `Valid`) now handles `Devnet`. `F3Manifest` returns nil for devnet (F3 does not run there); every other getter reads from the configured DevnetConfig. **net/glif/client.go** — two new lightweight JSON-RPC methods (`StateNetworkName`, `FetchGenesis`) so the devnet-init CLI can introspect a running lotus without pulling in Lotus API types. **cmd/lantern/devnet.go** — new `lantern devnet-init --lotus-rpc <URL>` subcommand: - Queries the running devnet lotus via three JSON-RPC calls (`Filecoin.StateNetworkName`, `Filecoin.ChainGetGenesis`, `Filecoin.ChainHead`). - Writes `<data-dir>/devnet/devnet-config.json` with the discovered identity (network name, genesis CID, lotus RPC, bootstrap peers). - Seeds `<data-dir>/devnet/bootstrap-anchor.json` from ChainHead so the daemon can start immediately after (no separate `lantern init` needed). - Refuses to overwrite existing files without `--force`. **cmd/lantern/main.go** — `cmdDaemon` now handles `--network devnet`: - Loads `devnet-config.json` from the network data dir. - Calls `build.ConfigureDevnet(cfg)` so all subsequent Network method calls see the devnet values. - Auto-enables `--insecure-anchor` (single-endpoint trust is honest for an operator-owned devnet). - Auto-enables `--insecure-gateway` (devnet lotus speaks HTTP; TLS on localhost adds no security). - Auto-disables `--auto-stale-reset` (the #118 quorum probe would only hit the same single endpoint; no value). - Routes `--gateway` at the devnet lotus by default. - Announces every implicit flag flip with a log line at boot. ## Trust posture The operator OWNS the devnet — `make devnet/up` on their own machine minted the genesis. So: - **Single-source trust is honest.** No multi-source quorum, no F3, no gateway/glif verification. - **Never touches keys, JWT, or tokens.** Same allow-list discipline as calibration + mainnet. - **`lantern reset --chain-state` still works** on a devnet data dir without wiping the devnet config (only `headerstore` + `bootstrap-anchor.json` are on the allow-list). ## Tests **7 unit tests in build/devnet_test.go:** - Save/Load roundtrip - Missing file → nil (cold boot) - Missing required fields → error - Configure/Get + defensive copy - Panic-with-help subtests for every Network method - All methods after Configure (including gossip topic + F3 nil check) - Atomic tmp+rename verification **3 end-to-end tests in cmd/lantern/devnet*_test.go:** - `TestDevnetInit_EndToEnd` — full devnet-init against a `httptest.NewServer` fake lotus RPC, asserts both files written with correct content. - `TestDevnetInit_RefusesOverwriteWithoutForce` — safety on re-init. - `TestDevnet_DevnetInitPersistsConfig` — full devnet-init → load → configure → Network method dispatch round-trip. This is the handshake the daemon uses at boot. Full `go test -count=1 -short ./...` green across the tree. ## Docs **docs/DEVNET.md** — quick-start (docker devnet → devnet-init → daemon), trust-posture explanation, tear-down + re-init flow, testing tips, cross-references to #118 + #119. ## What this unlocks - End-to-end soak testing of #119 mpool persistence at 4s block time: MpoolPushMessage → gossipsub → mine → StateSearchMsg → journal drop. - #118 auto-stale-reset validation on a compressed timescale (a 12h `--anchor-max-age` becomes ~1500 devnet epochs). - Full PDP proving loop at 4s epochs to hunt scheduling bugs faster than calibration allows. - The #104 Phase-1 tester-experience script can run against an isolated devnet instead of shared calibration. Butterflynet + any future Filecoin testnet becomes a trivial follow-up: drop-in another Network variant with hardcoded constants.
This was referenced Jul 11, 2026
Reiers
pushed a commit
that referenced
this pull request
Jul 12, 2026
…evnet Every change is gated on network == "devnet"; mainnet + calibration paths are byte-identical to pre-change behaviour. - eth_chainId + net_version return the devnet's actual EIP-155 chainId instead of the mainnet fallback 0x13a / "314". devnet-init reads eth_chainId from the running devnet lotus and persists it into devnet-config.json as ethChainID. This is the P0 finding: an EVM client signing with the wrong chainId gets "invalid signature" back from lotus and takes hours to diagnose. - Filecoin.Version.BlockDelay returns the devnet's actual cadence instead of hardcoded 30. devnet-init reads BlockDelay from Filecoin.Version and persists as blockDelaySecs. - Filecoin.StateNetworkName returns the devnet's localnet-uuid instead of the literal string "devnet". Handler now reads from the persisted devnet-config instead of a literal. Fixes gossipsub topic identity (/fil/blocks/<name>) and DHT protocol prefix alignment. - Filecoin.ChainGetGenesis on devnet returns the persisted genesis CID instead of "not implemented". Compile-time genesis constants exist only for mainnet + calibration; devnet's genesis is per-boot. - Filecoin.MpoolGetConfig returns a stable snapshot instead of HTTP 500. Values match lotus defaults so consumers (curio at boot) don't have to special-case Lantern. PriorityAddrs nil (marshals to JSON null, matching lotus's wire shape byte-for-byte). - Filecoin.MpoolPushMessage now works on single-node devnet. The gossipsub mesh can't form on a single-node docker devnet, so the standard mpool.Pool had nothing to publish onto. Daemon now wires a Pool whose Config.Sink POSTs directly to the devnet lotus via Filecoin.MpoolPush. All other Pool semantics (persist journal, pending set, nonce derivation, #47 reconcile/rebroadcast, #121 restart-persist) work identically; only the wire transport changes. Trust posture unchanged from #122: devnet is single-source by design (operator owns both the lotus and the Lantern client). - Devnet mode auto-wires the devnet lotus as VMBridge when the operator did not pass --vm-bridge-rpc. Fixes eth_getCode / eth_call / eth_getStorageAt timeouts on cold state (single-node devnet has no libp2p, so bitswap can't fetch state blocks; daemon fell through to errBridgeUnconfigured). - mpool.Config.Sink hook lets Pool run with a nil pubsub instance. When Sink is non-nil and ps is nil, New() skips the gossipsub join+subscribe entirely and Publish/Rebroadcast route through the sink. Only used by devnet mode; mainnet/calibration paths continue to require a pubsub instance. - net/glif.Client.EthChainID, .BlockDelaySecs, .MpoolPush — three new methods on the shared JSON-RPC client. Used by devnet-init (boot-time discovery) and by the daemon (devnet mpool sink). - build.DevnetConfig gained EthChainID + BlockDelaySecs fields. Older configs written before this change have zero values; handlers hint `lantern devnet-init --force` to re-capture from the running lotus. - 5 new unit tests in net/mpool/sink_test.go cover the nil-pubsub + Sink path: nil-pubsub requires Sink; Publish invokes Sink instead of gossipsub byte-identically; persist journal entry matches published raw; Close is a no-op on nil topic/subscription; sink error surfaces from Publish without recording pending. - Existing cmd/lantern devnet-init tests updated: fake lotus stub now answers eth_chainId + Filecoin.Version so the boot-time discovery succeeds; new asserts check EthChainID = 31415926 and BlockDelaySecs = 4 land in the persisted config. - Full suite green: go test ./... reports 51 packages ok, no failures. - ChainHead head-poll lag on devnet (#123 findings 8+9) and eth_getCode proxy-address slowness (finding 7). Both are latency issues, not correctness. Folding into a separate --devnet-head-poll-interval knob PR after this batch lands.
Reiers
added a commit
that referenced
this pull request
Jul 12, 2026
…evnet (#125) * fix(#123): close all ten devnet RPC parity gaps against local Curio devnet Every change is gated on network == "devnet"; mainnet + calibration paths are byte-identical to pre-change behaviour. - eth_chainId + net_version return the devnet's actual EIP-155 chainId instead of the mainnet fallback 0x13a / "314". devnet-init reads eth_chainId from the running devnet lotus and persists it into devnet-config.json as ethChainID. This is the P0 finding: an EVM client signing with the wrong chainId gets "invalid signature" back from lotus and takes hours to diagnose. - Filecoin.Version.BlockDelay returns the devnet's actual cadence instead of hardcoded 30. devnet-init reads BlockDelay from Filecoin.Version and persists as blockDelaySecs. - Filecoin.StateNetworkName returns the devnet's localnet-uuid instead of the literal string "devnet". Handler now reads from the persisted devnet-config instead of a literal. Fixes gossipsub topic identity (/fil/blocks/<name>) and DHT protocol prefix alignment. - Filecoin.ChainGetGenesis on devnet returns the persisted genesis CID instead of "not implemented". Compile-time genesis constants exist only for mainnet + calibration; devnet's genesis is per-boot. - Filecoin.MpoolGetConfig returns a stable snapshot instead of HTTP 500. Values match lotus defaults so consumers (curio at boot) don't have to special-case Lantern. PriorityAddrs nil (marshals to JSON null, matching lotus's wire shape byte-for-byte). - Filecoin.MpoolPushMessage now works on single-node devnet. The gossipsub mesh can't form on a single-node docker devnet, so the standard mpool.Pool had nothing to publish onto. Daemon now wires a Pool whose Config.Sink POSTs directly to the devnet lotus via Filecoin.MpoolPush. All other Pool semantics (persist journal, pending set, nonce derivation, #47 reconcile/rebroadcast, #121 restart-persist) work identically; only the wire transport changes. Trust posture unchanged from #122: devnet is single-source by design (operator owns both the lotus and the Lantern client). - Devnet mode auto-wires the devnet lotus as VMBridge when the operator did not pass --vm-bridge-rpc. Fixes eth_getCode / eth_call / eth_getStorageAt timeouts on cold state (single-node devnet has no libp2p, so bitswap can't fetch state blocks; daemon fell through to errBridgeUnconfigured). - mpool.Config.Sink hook lets Pool run with a nil pubsub instance. When Sink is non-nil and ps is nil, New() skips the gossipsub join+subscribe entirely and Publish/Rebroadcast route through the sink. Only used by devnet mode; mainnet/calibration paths continue to require a pubsub instance. - net/glif.Client.EthChainID, .BlockDelaySecs, .MpoolPush — three new methods on the shared JSON-RPC client. Used by devnet-init (boot-time discovery) and by the daemon (devnet mpool sink). - build.DevnetConfig gained EthChainID + BlockDelaySecs fields. Older configs written before this change have zero values; handlers hint `lantern devnet-init --force` to re-capture from the running lotus. - 5 new unit tests in net/mpool/sink_test.go cover the nil-pubsub + Sink path: nil-pubsub requires Sink; Publish invokes Sink instead of gossipsub byte-identically; persist journal entry matches published raw; Close is a no-op on nil topic/subscription; sink error surfaces from Publish without recording pending. - Existing cmd/lantern devnet-init tests updated: fake lotus stub now answers eth_chainId + Filecoin.Version so the boot-time discovery succeeds; new asserts check EthChainID = 31415926 and BlockDelaySecs = 4 land in the persisted config. - Full suite green: go test ./... reports 51 packages ok, no failures. - ChainHead head-poll lag on devnet (#123 findings 8+9) and eth_getCode proxy-address slowness (finding 7). Both are latency issues, not correctness. Folding into a separate --devnet-head-poll-interval knob PR after this batch lands. * fix(#123): auto-wire devnet VMBridge in standalone CLI + skip gossipsub mpool wiring on devnet Two follow-up fixes surfaced by a live smoke against a running docker devnet (curio-fork make docker/devnet, lotus v1.35.1, BlockDelay=4): 1. cmd/lantern/main.go standalone daemon didn't share the pkg/daemon VMBridge auto-wire I added in the previous commit. When `lantern daemon --network devnet` runs without an explicit --vm-bridge-rpc, any eth_call / eth_getCode / eth_getStorageAt returned "FEVM method requires --vm-bridge-rpc" instead of transparently using the devnet's own lotus. Now the CLI applies the same `--network devnet && !--vm-bridge-rpc → devnetCfg.LotusRPC` fallback. Verified live: after the fix, eth_getCode against the deployed PDPVerifier reaches the bridge (result is served instead of erroring). 2. pkg/daemon/start.go's startGossipHead was joining the gossipsub mempool topic on devnet EVEN THOUGH there's no gossipsub mesh on a single-node devnet. Once that mpool was in place, my devnet lotus-RPC sink block (which triggers only when chainAPI.Mpool == nil) never fired, so MpoolPushMessage silently dropped signed messages into the void. Fix: startGossipHead now short-circuits before the gossipsub mpool wiring on devnet, logs the reason, and leaves chainAPI.Mpool nil so the devnet sink block in startInternal activates. Bitswap wiring after the mpool block is preserved via a goto over a scoped variable-declaration block (Go-legal, verified with go build + go vet). Live-verified end-to-end on the docker devnet: Filecoin.StateNetworkName → "localnet-1069c685-..." (was "devnet") eth_chainId → 0x1df5e76 (31415926) (was 0x13a) net_version → "31415926" (was "314") Filecoin.Version.BlockDelay→ 4 (was 30) Filecoin.ChainGetGenesis → real CID (was not-implemented) Filecoin.MpoolGetConfig → lotus-shape defaults (was HTTP 500) vm-bridge auto-wired to devnet lotus (bridge log line + eth_getCode reaches bridge) eth_getCode is still slow on cold state (local hamt walk tries bitswap first, and single-node devnet has no bitswap peers, so the walk waits the full 15s timeout before falling through to the bridge). That's #123 finding #7 in a narrower form. Fix candidate: short-circuit the local-first order to bridge-first on devnet. Out of scope for this batch; filing a follow-up. Tests: full suite (`go test ./...`) 51/51 packages ok. --------- Co-authored-by: Nicklas Reiersen <nicklas@reiers.io>
Reiers
added a commit
that referenced
this pull request
Jul 12, 2026
…tStorageAt / eth_call (#126) Closes the last correctness gap from lantern#123 (finding 7). On a single-node docker devnet the local hamt walk in localEthGetCode / localEthGetStorageAt / localEthCall always burns its full retry budget before falling through to the bridge: there are no bitswap peers to fetch cold storage-trie blocks from. Symptom: eth_getCode against the deployed PDPVerifier proxy takes 15+ seconds (retry budget = 2 x 8s) and any real consumer (curio-core, ethers, viem) times out first. The devnet lotus IS the source of truth for the devnet chain (single- source by design, trust posture unchanged from #122), so the local walk adds latency without adding trust. Handlers now short-circuit straight to the auto-wired VMBridge when NetworkName == "devnet" && Bridge != nil. Live smoke, curio-fork docker devnet, height 17600+: eth_getCode(PDPVerifier) 34 ms (was 15 s timeout) eth_getStorageAt(slot 0) 28 ms (returns real 32-byte value) eth_call(bad selector) 13 ms (bridge returns correct revert) Mainnet + calibration paths byte-identical (bridge-first block gated on network == "devnet"). Escape hatch: if the operator explicitly drops the bridge on devnet (--vm-bridge-rpc=""), reads return errBridgeUnconfigured instead of silently hanging — same as pre-#123. Extracted the bridge-forward tail of EthCall into bridgeEthCall so the devnet short-circuit and the mainnet/calibration local-miss fallback share the same code. 5 new unit tests in rpc/handlers/devnet_bridge_first_test.go: - devnet EthGetCode / EthGetStorageAt / EthCall each call bridge exactly once (never touch local) - devnet EthGetCode / EthCall without a bridge return errBridgeUnconfigured - mainnet EthGetCode with no accessor still falls back to bridge (local path unchanged) Full suite: go test ./... reports 52 packages ok, no failures. Co-authored-by: Nicklas Reiersen <nicklas@reiers.io>
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.
Adds a
build.DevnetNetwork variant and alantern devnet-initcommand so Lantern can point at a locally-hosted Curio docker devnet (curio-fork/docker/,make devnet/up) and run against it as a first-class network.Why
The docker devnet runs at 4s block time (
build/params_2k.goin the curio fork) with FWSS + PDP + Multicall3 + registry contracts pre-deployed. That's a much faster loop for exercising the full Lantern + Curio Core stack than calibration (30s epochs, external dependency) or mainnet (real FIL).The blocker was that Lantern hardcoded
Mainnet+Calibrationas the onlybuild.Networkvariants: no bootstrap peers, no gossip topics, no genesis CID, no wire-name mapping for a devnet.What's in
build/devnet.go(new)Devnet Network = \"devnet\"constant.DevnetConfigstruct capturing the per-boot devnet identity:NetworkName,GenesisCID,LotusRPC,BootstrapPeers.ConfigureDevnet(cfg)runtime setter +GetDevnetConfig()accessor +IsDevnetConfigured()predicate.LoadDevnetConfig(path)/SaveDevnetConfig(path, cfg)for the on-disk JSON at<data-dir>/devnet/devnet-config.json, atomic tmp+rename write.ConfigureDevnet— pointing operators atlantern devnet-initin the error message.build/network.goEvery getter switch (
NetworkName,GenesisCID,GossipTopicBlocks/Messages,BootstrapPeers,F3Manifest,Valid) now handlesDevnet.F3Manifestreturnsnilfor devnet (F3 does not run there); every other getter reads from the configuredDevnetConfig.net/glif/client.goTwo new lightweight JSON-RPC methods (
StateNetworkName,FetchGenesis) so the devnet-init CLI can introspect a running lotus without pulling in Lotus API types.cmd/lantern/devnet.go(new)lantern devnet-init --lotus-rpc <URL>:Filecoin.StateNetworkName,Filecoin.ChainGetGenesis,Filecoin.ChainHead).<data-dir>/devnet/devnet-config.jsonwith the discovered identity.<data-dir>/devnet/bootstrap-anchor.jsonfromChainHeadso the daemon can start immediately — no separatelantern init.--force.cmd/lantern/main.gocmdDaemonnow handles--network devnet:devnet-config.jsonfrom the network data dir and callsbuild.ConfigureDevnet(cfg)so all subsequent Network method calls see the devnet values.--insecure-anchor(single-endpoint trust is honest for an operator-owned devnet).--insecure-gateway(devnet lotus speaks HTTP; TLS on localhost adds no security).--auto-stale-reset(the bridge-off boot: auto-reset stale anchor + headerstore when persisted state is beyond parentWalkCap (#51 gap) #118 quorum probe would only hit the same single endpoint; no value).--gatewayat the devnet lotus by default.Trust posture
The operator OWNS the devnet —
make devnet/upon their own machine minted the genesis. So:lantern reset --chain-statestill works on a devnet data dir without wiping the devnet config (onlyheaderstore+bootstrap-anchor.jsonare on the allow-list, sodevnet-config.jsonsurvives).Tests
7 unit tests in
build/devnet_test.go:TestDevnetConfig_SaveThenLoadRoundtripTestDevnetConfig_LoadMissingIsNilNilTestDevnetConfig_LoadRejectsMissingRequiredTestDevnetConfig_ConfigureAndGet(includes defensive-copy verification)TestDevnet_MethodsPanicWhenUnconfigured(5 subtests, one per Network method)TestDevnet_MethodsAfterConfigure(all methods including gossip topic + F3 nil check)TestDevnet_SaveConfigAtomicRename(no orphan.tmpleft behind)3 end-to-end tests in
cmd/lantern/devnet*_test.go:TestDevnetInit_EndToEnd— full devnet-init against ahttptest.NewServerfake lotus RPC, asserts both files written with correct content.TestDevnetInit_RefusesOverwriteWithoutForce— safety on re-init.TestDevnet_DevnetInitPersistsConfig— full devnet-init → load → configure → Network method dispatch round-trip. This is the handshake the daemon uses at boot.Full
go test -count=1 -short ./...green across the tree.Docs
docs/DEVNET.md— quick-start (docker devnet → devnet-init → daemon), trust-posture explanation, tear-down + re-init flow, testing tips, cross-references to #118 + #119.What this unlocks
MpoolPushMessage→ gossipsub → mine →StateSearchMsg→ journal drop.--anchor-max-agebecomes ~1500 devnet epochs).Not in this PR
dockerdirectory and Makefile targets already exist upstream (they live incurio-fork/docker/), so nothing needs to change there.Quick start (once docker is available)