Skip to content

fix(#123): close all ten devnet RPC parity gaps against local Curio devnet#125

Merged
Reiers merged 2 commits into
mainfrom
fix/123-devnet-rpc-parity
Jul 12, 2026
Merged

fix(#123): close all ten devnet RPC parity gaps against local Curio devnet#125
Reiers merged 2 commits into
mainfrom
fix/123-devnet-rpc-parity

Conversation

@Reiers

@Reiers Reiers commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Closes #123.

Every change is gated on network == "devnet"; mainnet + calibration paths are byte-identical to pre-change behaviour.

What changes (per finding on #123)

P0 correctness (breaks EVM clients silently):

P1 correctness (breaks gossipsub identity + curio schedulers):

P2 correctness (unblocks send-path + peace-of-mind restart tests):

  • [Architecture Questions #10] 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. Unlocks the entire peace-of-mind restart drill on devnet, which is our fastest iteration loop.
  • [JWT tokens are issued without exp claims and never expire #7] eth_getCode / eth_call / eth_getStorageAt cold-state timeouts. Devnet mode auto-wires the devnet lotus as the VMBridge when the operator didn't pass --vm-bridge-rpc. A single-node devnet has no libp2p, so bitswap can't fetch state blocks; the daemon fell through to errBridgeUnconfigured.
  • [Beacon cert-exchange upstream should default to other Lantern beacons, not Glif #6] Filecoin.MpoolGetConfig returns a stable snapshot instead of HTTP 500. Values chosen to match lotus defaults. PriorityAddrs is nil (marshals to JSON null, matching lotus's wire shape byte-for-byte); an empty slice would marshal as [] and break consumers that check == null.

Trust posture unchanged from #122: devnet is single-source by design (operator owns both the devnet lotus and the Lantern client).

New surfaces

  • 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 and reject nil-pubsub construction.
  • 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.

Tests

  • 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 + BlockDelaySecs = 4 land in the persisted config; discovery-call floor raised from 3 to 5.
  • Full suite green: go test ./... reports 51 packages ok, no failures.

Not in this PR

Follow-up

Live devnet smoke deferred (colima needs restart + fresh docker compose up). Unit tests cover the parse + persist + handler-read paths end-to-end via an in-process fake lotus, but the truest test is against a running make docker/devnet node. I'll capture that in a follow-up comment on #123 once colima is back up.

@Reiers

Reiers commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

Live-smoke verification against running docker devnet

Booted a fresh docker compose up -d under projects/curio-fork/docker/ (lotus v1.35.1, BlockDelay=4, network localnet-1069c685-ccf0-49e4-8e2c-f2b4a4ed7f7e, chainId 31415926) plus lantern devnet-init + lantern daemon --network devnet on the same binary as this branch HEAD.

devnet-init discovery

✓ StateNetworkName: "localnet-1069c685-ccf0-49e4-8e2c-f2b4a4ed7f7e"
✓ Genesis:          bafy2bzaceahwbexzeywl2pd3hm2ggh5w7fhfibuf2v7v5wig5us363b75ku44
✓ ChainHead:        epoch 15538, state root bafy2bzacect3x7k7lxduvxnpkbrjj4ulki7rqfid7msnm64fwfd7ktq6m5m5y
✓ eth_chainId:      31415926 (0x1df5e76)
✓ BlockDelaySecs:   4

RPC parity (6 fixes verified live)

Method Result Was
Filecoin.StateNetworkName localnet-1069c685-...-a4ed7f7e "devnet"
Filecoin.Version.BlockDelay 4 30
Filecoin.ChainGetGenesis genesis CID not implemented
eth_chainId 0x1df5e76 0x13a
net_version "31415926" "314"
Filecoin.MpoolGetConfig {PriorityAddrs: null, SizeLimitHigh: 30000, ...} HTTP 500

Boot log includes:

  vm-bridge:    devnet auto-wire (lantern#123) → http://127.0.0.1:1234/rpc/v1
  vm-bridge:    forest@127.0.0.1:1234/rpc/v1

Partial (see finding #7)

eth_getCode on the real PDPVerifier proxy address (0x564Fce2BeCEdd3042F0Cb8bE787fe8230a40d276) reaches the auto-wired bridge but still times out at 15s. Lotus directly serves the same call in <1s. Root cause: eth_getCode tries local hamt walk (state cache → bitswap → gateway) before falling through to the bridge, and single-node devnet has no bitswap peers so the walk waits its full timeout. Fix candidate is a devnet-only short-circuit that goes bridge-first for these methods; out of scope for this PR. Filing a follow-up.

Follow-ups (out of scope, worth their own issue/PR)

  • --devnet-head-poll-interval knob to close the head-lag findings (ChainHead + eth_blockNumber lag by ~80 epochs on single-node devnet since gossipsub can't propagate live head at 4s cadence)
  • devnet-only bridge-first ordering for read-side eth_* methods (finding JWT tokens are issued without exp claims and never expire #7 in its narrower form)
  • standalone lantern daemon CLI doesn't wire mpool at all today (this PR's mpool sink is in pkg/daemon for embedded consumers like curio-core). MpoolPushMessage on the standalone CLI has always been broken; adding baseline gossipsub mpool wiring to cmd/lantern (with devnet sink fallback) is a separate PR.

Nicklas Reiersen added 2 commits July 12, 2026 13:43
…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.
…ub 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.
@Reiers Reiers force-pushed the fix/123-devnet-rpc-parity branch from dca3034 to 2eceb8f Compare July 12, 2026 11:44
@Reiers Reiers merged commit 035a291 into main Jul 12, 2026
1 check passed
@Reiers Reiers deleted the fix/123-devnet-rpc-parity branch July 12, 2026 11:49
Reiers added a commit that referenced this pull request Jul 12, 2026
#127)

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.

Co-authored-by: Nicklas Reiersen <nicklas@reiers.io>
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.

[devnet] RPC parity gaps against local Curio devnet lotus (post-#122)

1 participant