Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ The "installer actually upgrades when a new release is out" release.
re-ran the installer kept getting v1.2.x behaviour (asks for a passphrase
every daemon boot because that version writes its keystore under
`~/.lantern/keystore` instead of the network-split `~/.lantern/<net>/keystore`
used by v1.5+). Reported by Nicklas, 2026-05-28.
used by v1.5+). Reported 2026-05-28.
- **New behaviour:** when a local binary exists, the installer fetches the
published `.sha256` for the requested version and compares. Match →
skip. Differ → upgrade. Offline / sha unreachable → keep the legacy
Expand Down Expand Up @@ -606,7 +606,7 @@ The "installer actually works on a fresh Mac" release.
caller-set `LANTERN_PREFIX` → `/opt/homebrew/bin` → `/usr/local/bin`
→ `~/.local/bin` (created on demand). When the chosen target is not on
PATH, the installer prints the exact `export PATH=...` line to add to
the shell profile. Reported by Nicklas, 2026-05-28.
the shell profile. Reported 2026-05-28.

- **Symlink self-heal on re-runs.** The symlink was only created from the
fresh-download branch; users with an existing binary at `~/.lantern/lantern`
Expand Down
49 changes: 44 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

*Boots in minutes. Verifies everything. No 76 GB snapshot. No third-party trust.*

*Now in three tiers — Light, PDP, and a pure-Go Full node — chosen at install time.*

[![CI](https://github.com/Reiers/lantern/actions/workflows/ci.yml/badge.svg)](https://github.com/Reiers/lantern/actions/workflows/ci.yml)
[![License: Apache 2.0 OR MIT](https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue.svg)](#license)
[![Release: v1.8.4](https://img.shields.io/badge/release-v1.8.4-0090ff.svg)](https://github.com/Reiers/lantern/releases)
[![Release: v1.9.0-rc1](https://img.shields.io/badge/release-v1.9.0--rc1-0090ff.svg)](https://github.com/Reiers/lantern/releases)
[![Go: 1.25+](https://img.shields.io/badge/go-1.25%2B-00ADD8.svg)](go.mod)

**One line to install:**
Expand All @@ -26,7 +28,17 @@

## TL;DR

Lantern is a pure-Go Filecoin light node. **~40 MB binary, ~1 GB working state, zero CGo, no `filecoin-ffi`, no Rust toolchain.** It serves a Lotus-compatible JSON-RPC (71 / 71 of the Curio `FULLNODE_API` surface, plus the `eth_*` surface needed by FoC clients) and verifies every byte locally against BLS, F3, DRAND, and IPLD content addressing. No trusted RPC provider. No 76 GB snapshot.
Lantern is a pure-Go Filecoin node. **~40 MB binary, zero CGo, no `filecoin-ffi`, no Rust toolchain.** It serves a Lotus-compatible JSON-RPC (71 / 71 of the Curio `FULLNODE_API` surface, plus the `eth_*` surface needed by FoC clients) and verifies every byte locally against BLS, F3, DRAND, and IPLD content addressing. No trusted RPC provider. No 76 GB snapshot.

**Lantern now installs in three tiers, chosen up front so a light client never carries a heavy footprint:**

| Tier | Footprint | For |
|------|-----------|-----|
| **Light Node** | ~1 GB, in-memory | Clients, wallets, chain reads, SP backup node |
| **PDP Node** (mid) | persistent 2–5 GB | PDP prove/settle + backup block producer |
| **Full Node** | tunable low-GB (not 76) | A real, snapshot-free full node — *in active development* |

> **🚧 Lantern is undergoing a major expansion.** It began as a light client; it is growing into a full node you can run on a Mac mini — snapshot-free boot, tunable single-digit-GB on-disk footprint, and pure-Go block validation with **no Rust**. See [the Full Node epic (#87)](https://github.com/Reiers/lantern/issues/87). The Light and PDP tiers are production-shape today; the Full tier is landing tier by tier.

**Current release:** [v1.8.4](https://github.com/Reiers/lantern/releases/tag/v1.8.4) on mainnet + calibration. **In production today** as the chain backend embedded in [Curio Core](https://curiocore.io) (which ran a full PDP hot-storage flow on Filecoin **mainnet** end-to-end on Lantern) and as a secondary node on the mainnet SP `f03678816` (sp.reiers.io).

Expand All @@ -40,10 +52,12 @@ Lantern is the middle path: a real Filecoin node with real cryptographic verific

### Who is this for?

- **🌱 Filecoin users.** Hold your own keys, send and receive FIL, look up balances and miners, without trusting Glif or any custodial RPC. Your wallet is your wallet.
- **🪪 SP operators.** Run Lantern as a secondary chain node next to your primary Lotus or Forest. If the primary stalls during a WdPost window, Curio fails over to Lantern and your sectors stay current.
- **🌱 Filecoin users.** Hold your own keys, send and receive FIL, look up balances and miners, without trusting Glif or any custodial RPC. Your wallet is your wallet. *(Light Node.)*
- **🪪 SP operators.** Run Lantern as a secondary chain node next to your primary Lotus or Forest. If the primary stalls during a WdPost window, Curio fails over to Lantern and your sectors stay current. *(Light or PDP Node.)*
- **🧱 PDP providers.** Run the PDP prove/settle loop on a persistent, restart-warm cache without standing up a 76 GB Lotus sidecar. *(PDP Node.)*
- **📐 SP / FoC client embedders.** Pull `pkg/daemon` into your Go binary and run a fully verified Filecoin chain backend in-process. [Curio Core](https://github.com/Reiers/curio-core) does exactly this.
- **🔧 Tooling developers.** A real Lotus-compatible RPC on your laptop, no rate limits. Run `eth_*` against a node you control.
- **🖥️ Full-node runners.** Want a real, snapshot-free Filecoin full node on a Mac mini, pure-Go, no Rust? That's the Full Node tier, in active development. *(Full Node.)*

---

Expand All @@ -55,7 +69,13 @@ Lantern is the middle path: a real Filecoin node with real cryptographic verific
curl -fsSL https://get.golantern.io | bash
```

This downloads the latest signed release binary, walks you through a multi-source trust quorum to anchor the node, sets up the wallet, and offers to install Lantern as a background service. Three minutes start to finish.
This downloads the latest signed release binary, **asks which node tier you want (Light / PDP / Full)**, walks you through a multi-source trust quorum to anchor the node, sets up the wallet, and offers to install Lantern as a background service. Three minutes start to finish. The tier is chosen *at install time* (not a runtime flag) so a Light node genuinely stays light — only the tier you pick provisions the larger cache and footprint.

```
l Light Node — ~1 GB. Clients, wallets, backup node. Smallest footprint. (default)
p PDP Node — persistent 2–5 GB cache + prove/settle + backup block producer
f Full Node — snapshot-free full node, serves the whole chain (in development)
```

Once it's running:

Expand All @@ -80,6 +100,25 @@ Three views via the pill switcher in the top right:

Dark mode follows your OS setting. Zero JS framework dependencies, ~34 KB total, all embedded in the binary.

## Node tiers

Lantern is one binary that installs as one of three tiers. You pick the tier **at install time**; the daemon reads the persisted choice at start. This keeps a Light node genuinely light — the PDP and Full footprints are opt-in, not carried by everyone.

### Light Node
The original Lantern. ~1 GB, in-memory block cache. Wallets, chain reads, `eth_*`, the full 71-method Curio RPC surface, and an SP **backup node** role (fail-over chain backend next to a primary Lotus/Forest). Production-shape today.

### PDP Node (mid)
Everything Light does, plus a **persistent** 2–5 GB block cache whose warm PDP/payments/registry/USDFC contract state **survives restart** (a restarted node doesn't cold-fetch its whole warm set mid proving-window), plus the full write surface including **block production** — so it can run the PDP prove/settle loop and double as a backup block producer. Block production requires a VM bridge for a valid post-execution state root. Production-shape today.

### Full Node — *in active development*
A real, snapshot-free Filecoin full node that runs on a Mac mini:

- **Snapshot-free boot.** Anchors at a recent F3-finalized tipset via a multi-source quorum, then fills history over BlockSync + follows head over gossip. No 76 GB snapshot, no genesis re-execution, minutes not days. The anchor's multi-source, BLS-verified, F3-finalized quorum is a *stronger* boot trust than downloading a single-source snapshot.
- **Tunable low-GB footprint.** Keeps headers, F3 certs, DRAND, and actor code always; recent state/messages/receipts for a tunable number of finalities; drops older data (safely re-fetchable, CID-verified). Single-digit-GB steady state instead of ~76 GB, tunable up for archive roles.
- **Pure-Go block validation, no Rust.** Re-verifies each block's signature, election + ticket VRF, miner eligibility, and win-count in pure Go. The two proof-heavy pieces — WinningPoSt SNARK verify and FVM re-execution — are currently anchored to F3 finality (the network's own consensus), with pure-Go implementations of both on the roadmap so a Full node can eventually re-derive them itself with zero Rust.

Full-node work is tracked under [the Full Node epic (#87)](https://github.com/Reiers/lantern/issues/87) and lands tier by tier.

## How it works

Filecoin's state is a content-addressed IPLD merkle tree (HAMTs and AMTs all the way down). If you have a verified state root, you don't need the whole tree — you need the *path* from the root to the actor you care about. That path is a handful of nodes, KBs not GBs, and every node is self-verifying against its parent by CID.
Expand Down
2 changes: 1 addition & 1 deletion TRUST-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ A short answer to "what can attackers controlling X do to a Lantern user?"
| The default HTTPS gateway (gateway.lantern.reiers.io) | DoS only. Every byte they serve is locally CID-verified. |
| The Glif fallback RPC | DoS only. Same verification. |
| One or more libp2p Bitswap peers | DoS only. Same verification. |
| All currently-connected gossipsub peers | Stale chain head (a fork) but never a chain-rule-violating one. F3 finality re-anchors eventually. |
| All currently-connected gossipsub peers (eclipse) | At most a stale/lighter fork on the *un-finalized* tip, never a chain-rule-violating or finalized-state lie. Defenses stack: heaviest-ParentWeight fork choice (#79) forces an attacker to out-*weight* the real chain (control real winning power) rather than just spam sybil peers; trusted bootstrap/beacon peers are connmgr-protected and un-evictable (#80) so the peer table can't simply be crowded out; the running-head divergence monitor (`chain/headcheck`, #85) cross-checks the head against a diversity of independent observers (counted by source kind) and raises an eclipse alarm on a >3-epoch divergence; and the header-propagation gate only re-gossips CID-verified blocks so a Lantern node can't be turned into an amplifier for a fake chain. F3 finality fully closes the tip exposure once active on mainnet. |
| The operator's wired Bridge (when configured) | Wrong `StateCall` receipts and wrong post-execution stateRoots for blocks the operator publishes. NOT: header acceptance, F3, state reads. |
| The operator's local disk (Badger cache) | DoS by corruption. Reads still get re-verified on next access. Loss of mempool history. |
| The operator's local network | DoS only. Lantern doesn't talk plaintext for anything security-bearing (RPC + gossipsub are TLS / secio). |
Expand Down
36 changes: 36 additions & 0 deletions chain/fullvalidate/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Package fullvalidate implements the pure-Go, full-node block-validation
// pipeline for Lantern's Full node tier (issue #90, part of the #87 full-node
// epic).
//
// # What this is
//
// A Lantern Full node follows and serves the whole chain. On top of the Phase-1
// structural/beacon checks in chain/header.ValidateHeader, a Full node also has
// the chain STATE resident (via the F3-anchored, CID-verified cache), so it can
// look up a block's worker key and miner power and run the remaining consensus
// checks that a light client cannot. This package is that second pass.
//
// It lifts the Lotus filcns block-validation check set MINUS the two filecoin-ffi
// (Rust) calls:
//
// - VerifyWinningPoSt (Groth16/BLS12-381 SNARK) -> deferred to proofs/winningpost (#88)
// - TipSetState (FVM message re-execution) -> deferred to Stage C wazero (#89)
//
// Everything this package DOES run is pure-Go BLS / arithmetic that Lantern
// already ships (crypto/sigs, chain/beacon, chain/types.ElectionProof):
//
// - block signature over the worker key (crypto/sigs.CheckBlockSignature)
// - election-proof VRF (VerifyVRF over drand-derived base)
// - ticket VRF (VerifyVRF)
// - win-count vs miner/total quality-adjusted power (ElectionProof.ComputeWinCount)
// - claimed-winner sanity (WinCount >= 1)
//
// # Trust model
//
// With this package a Full node independently re-verifies the VRF/signature/
// win-count consensus of every ingested block. It still TRUSTS F3 finality for
// the WinningPoSt SNARK and the FVM state transition it does not natively run.
// That boot/finality trust is a multi-source, BLS-verified, 2/3-power quorum -
// strictly stronger than a single-source snapshot import. #88 and #89 close the
// two remaining ffi gaps to make the node fully trustless.
package fullvalidate
165 changes: 165 additions & 0 deletions chain/fullvalidate/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package fullvalidate

import (
"bytes"
"context"
"errors"
"fmt"

"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
gscrypto "github.com/filecoin-project/go-state-types/crypto"

"github.com/Reiers/lantern/chain/beacon"
"github.com/Reiers/lantern/chain/types"
"github.com/Reiers/lantern/crypto/sigs"
)

// StateView is the read surface a Full node exposes over its resident,
// F3-anchored, CID-verified state. It is deliberately small: the pipeline only
// needs the worker key and the power split to validate a block's consensus.
//
// Both methods take the block's Miner address and are resolved against the
// PARENT tipset's state (the state the block was produced on top of).
type StateView interface {
// WorkerKey resolves miner -> its current worker's pubkey address
// (BLS/secp), i.e. StateMinerInfo(miner).Worker then StateAccountKey.
WorkerKey(ctx context.Context, miner address.Address) (address.Address, error)

// MinerQAPower returns (thisMinerQAPower, networkTotalQAPower) so the
// win-count can be recomputed. Matches StateMinerPower semantics.
MinerQAPower(ctx context.Context, miner address.Address) (minerPow, totalPow abi.StoragePower, err error)
}

// Result reports which checks ran. WinningPoStVerified / StateReExecuted stay
// false until #88 / #89 land; a caller can see exactly how much was trustlessly
// verified versus F3-trusted.
type Result struct {
SignatureOK bool
ElectionVRFOK bool
TicketVRFOK bool
EligibilityOK bool
WinCountOK bool
WinningPoStVerified bool // #88: pure-Go SNARK verify (not yet)
StateReExecuted bool // #89: pure-Go FVM (not yet)
}

// ValidateBlockConsensus runs the pure-Go consensus checks a Full node can do
// with resident state. It assumes chain/header.ValidateHeader already passed
// (structural + beacon + parent linkage). `prevBeacon` is the latest beacon
// entry from the parent epoch, used when the block carries no entries of its
// own. `sv` reads the PARENT state.
//
// It does NOT run WinningPoSt SNARK verify or FVM re-execution; those remain
// F3-trusted until #88/#89.
func ValidateBlockConsensus(
ctx context.Context,
bh *types.BlockHeader,
prevBeacon *types.BeaconEntry,
sv StateView,
) (Result, error) {
var res Result
if bh == nil {
return res, errors.New("fullvalidate: nil block header")
}
if sv == nil {
return res, errors.New("fullvalidate: nil state view")
}
if bh.ElectionProof == nil {
return res, errors.New("fullvalidate: nil election proof")
}
if bh.Ticket == nil {
return res, errors.New("fullvalidate: nil ticket")
}

// Worker key from parent state (pubkey-typed, ready for sigs.Verify).
waddr, err := sv.WorkerKey(ctx, bh.Miner)
if err != nil {
return res, fmt.Errorf("fullvalidate: resolve worker key: %w", err)
}

// (1) Block signature over the worker key.
if err := sigs.CheckBlockSignature(ctx, bh, waddr); err != nil {
return res, fmt.Errorf("fullvalidate: block signature: %w", err)
}
res.SignatureOK = true

// Randomness base: the beacon entry to draw from is the block's last
// entry if present, else the parent-epoch beacon. Matches Lotus.
rBeaconData, err := randBeaconData(bh, prevBeacon)
if err != nil {
return res, err
}

// Miner address CBOR is the entropy for both VRF draws.
minerEntropy, err := minerCBOR(bh.Miner)
if err != nil {
return res, err
}

// (2) Election-proof VRF.
evrfBase, err := beacon.DrawRandomnessFromBase(
rBeaconData, gscrypto.DomainSeparationTag_ElectionProofProduction, bh.Height, minerEntropy)
if err != nil {
return res, fmt.Errorf("fullvalidate: draw election randomness: %w", err)
}
if err := verifyVRF(waddr, evrfBase, bh.ElectionProof.VRFProof); err != nil {
return res, fmt.Errorf("fullvalidate: election VRF: %w", err)
}
res.ElectionVRFOK = true

// (3) Ticket VRF (same worker key, ticket domain).
tvrfBase, err := beacon.DrawRandomnessFromBase(
rBeaconData, gscrypto.DomainSeparationTag_TicketProduction, bh.Height, minerEntropy)
if err != nil {
return res, fmt.Errorf("fullvalidate: draw ticket randomness: %w", err)
}
if err := verifyVRF(waddr, tvrfBase, bh.Ticket.VRFProof); err != nil {
return res, fmt.Errorf("fullvalidate: ticket VRF: %w", err)
}
res.TicketVRFOK = true

// (4) Win-count sanity + recomputation against parent power.
if bh.ElectionProof.WinCount < 1 {
return res, errors.New("fullvalidate: block does not claim to be a winner")
}
minerPow, totalPow, err := sv.MinerQAPower(ctx, bh.Miner)
if err != nil {
return res, fmt.Errorf("fullvalidate: miner power: %w", err)
}
want := bh.ElectionProof.ComputeWinCount(types.BigInt{Int: minerPow.Int}, types.BigInt{Int: totalPow.Int})
if bh.ElectionProof.WinCount != want {
return res, fmt.Errorf("fullvalidate: wrong win count: claims %d, computed %d",
bh.ElectionProof.WinCount, want)
}
res.WinCountOK = true

// WinningPoStVerified / StateReExecuted stay false: F3-trusted until #88/#89.
return res, nil
}

// verifyVRF checks a BLS VRF proof against the worker key. A VRF proof in
// Filecoin is a BLS signature over the drawn randomness base; sigs.Verify
// dispatches to the registered BLS verifier (pure-Go gnark-crypto).
func verifyVRF(worker address.Address, base, vrfProof []byte) error {
sig := &gscrypto.Signature{Type: gscrypto.SigTypeBLS, Data: vrfProof}
return sigs.Verify(sig, worker, base)
}

func randBeaconData(bh *types.BlockHeader, prevBeacon *types.BeaconEntry) ([]byte, error) {
if len(bh.BeaconEntries) != 0 {
return bh.BeaconEntries[len(bh.BeaconEntries)-1].Data, nil
}
if prevBeacon == nil {
return nil, errors.New("fullvalidate: no beacon entry available (block carries none and no prev)")
}
return prevBeacon.Data, nil
}

func minerCBOR(miner address.Address) ([]byte, error) {
buf := new(bytes.Buffer)
if err := miner.MarshalCBOR(buf); err != nil {
return nil, fmt.Errorf("fullvalidate: marshal miner addr: %w", err)
}
return buf.Bytes(), nil
}
Loading
Loading