headcheck

package
v1.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: Apache-2.0, MIT Imports: 13 Imported by: 0

Documentation

Overview

Package headcheck is Lantern's running-head divergence monitor.

Background (#85, community discussion, #80): Lantern follows the *running* (unfinalized) chain head over gossipsub (net/blockingest). The boot anchor is a strong multi-source 5-of-N quorum on an F3-finalized tipset (chain/bootstrap), and #79 added heaviest-ParentWeight fork choice so a competing *lighter* fork on the running head is rejected. What neither of those closes is the case where an attacker eclipses the gossip peer table and feeds a self-consistent, parent-linked, *heavier-looking* fork: the node would happily follow it because every individual header hashes fine and the weight arithmetic only sees the attacker's numbers.

headcheck is the defense-in-depth layer #85 item 2 asks for: it periodically asks several INDEPENDENT head sources (gossip-observed tip, one or more Lotus-compatible RPC endpoints, user --peer endpoints) "what epoch is the head at?" and checks that they agree within a small look-back tolerance (default 3 blocks, matching the #85 ask). When the local gossip head drifts outside that tolerance from the diversity of external sources, headcheck raises a divergence signal so the daemon can log loudly / surface it on the dashboard / (optionally) refuse to serve a head it can't corroborate.

It is explicitly NOT a trusted-RPC head oracle. Lantern never *takes* its head from an RPC (that is the whole point of the project, see TRUST-MODEL.md §3.1). headcheck only uses external sources to CORROBORATE or DISPUTE the head Lantern already derived from gossip. A single RPC saying something different does not move our head; a diverse quorum of independent sources disagreeing with us is an eclipse alarm.

Source diversity matters: N sources that are really the same upstream (e.g. three Glif URLs) are one source for eclipse purposes. headcheck counts agreement by source Kind so a quorum requires genuinely independent observers, mirroring chain/bootstrap's Kind policy.

This package is pure logic over a HeadSource interface; the libp2p / HTTP source adapters live with the daemon wiring so headcheck stays unit-testable with mocks.

Index

Constants

View Source
const DefaultInterval = 30 * time.Second

DefaultInterval is how often the monitor polls its sources.

View Source
const DefaultLookback abi.ChainEpoch = 3

DefaultLookback is the head-agreement tolerance in epochs. #85 asks for a 3-block look-back: a source up to 3 epochs behind our head (or ahead of it) still counts as "agreeing", since gossip propagation and null rounds routinely put honest observers a couple epochs apart.

View Source
const DefaultMinAgree = 2

DefaultMinAgree is the minimum number of distinct-Kind external sources that must report a head within Lookback of ours before we treat the local head as corroborated. With fewer than this many *reachable* sources headcheck reports StatusInsufficient rather than a false-clean or a false-alarm.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Local reports Lantern's own (gossip-derived) head epoch. Required.
	Local func() abi.ChainEpoch
	// Sources are the external observers. Polled in parallel each round.
	Sources []HeadSource
	// Lookback tolerance in epochs (default DefaultLookback).
	Lookback abi.ChainEpoch
	// Interval between rounds (default DefaultInterval).
	Interval time.Duration
	// MinAgree distinct-Kind sources required to corroborate
	// (default DefaultMinAgree).
	MinAgree int
	// PerSourceTimeout caps each source's HeadEpoch call (default 15s).
	PerSourceTimeout time.Duration
	// OnResult, if set, fires after each round with the Result. Used by
	// the daemon to log alarms and update the dashboard. May be nil.
	OnResult func(Result)
}

Config configures a Monitor.

type GatewayHeadSource

type GatewayHeadSource struct {
	// contains filtered or unexported fields
}

GatewayHeadSource is a HeadSource backed by the Lantern gateway's HTTP /state/root endpoint (net/hsync shape), which returns an Epoch field. The gateway speaks HTTP, not Filecoin.ChainHead JSON-RPC, so it needs its own adapter. Tagged KindLanternGateway so diversity counting treats it as its own kind (and, per bootstrap policy, does not let the project gateway alone satisfy a quorum). Purely corroborative, never the head oracle - same contract as RPCHeadSource.

func NewGatewayHeadSource

func NewGatewayHeadSource(url string, timeout time.Duration) *GatewayHeadSource

NewGatewayHeadSource builds a gateway-backed head source. url is the gateway base (e.g. https://gateway.golantern.io); zero timeout => 15s.

func (*GatewayHeadSource) HeadEpoch

func (s *GatewayHeadSource) HeadEpoch(ctx context.Context) (abi.ChainEpoch, error)

func (*GatewayHeadSource) Kind

func (s *GatewayHeadSource) Kind() bootstrap.Kind

func (*GatewayHeadSource) Name

func (s *GatewayHeadSource) Name() string

type HeadSource

type HeadSource interface {
	// Name is a stable human label for logs/dashboard.
	Name() string
	// Kind classifies the source for diversity counting.
	Kind() bootstrap.Kind
	// HeadEpoch returns the source's current head epoch.
	HeadEpoch(ctx context.Context) (abi.ChainEpoch, error)
}

HeadSource is a single independent observer of the chain head epoch. Implementations: the gossip-observed tip (local), a Lotus-compatible RPC endpoint, a user --peer endpoint, a libp2p peer. Kind is used to measure genuine independence (see package doc).

type Monitor

type Monitor struct {
	// contains filtered or unexported fields
}

Monitor periodically checks local-vs-external head agreement.

func New

func New(cfg Config) *Monitor

New builds a Monitor. Local is required; with no Sources every round is StatusInsufficient (the monitor is then a no-op alarm-wise, which is the correct behaviour for a node the operator hasn't given any corroborating endpoints).

func (*Monitor) CheckOnce

func (m *Monitor) CheckOnce(ctx context.Context) Result

CheckOnce runs a single round synchronously and returns the Result. Exported for tests and for an on-demand dashboard refresh.

func (*Monitor) Last

func (m *Monitor) Last() Result

Last returns the most recent round's Result.

func (*Monitor) Start

func (m *Monitor) Start(ctx context.Context)

Start runs the monitor loop until ctx is cancelled or Stop is called. Non-blocking: spawns a goroutine.

func (*Monitor) Stats

func (m *Monitor) Stats() (rounds, diverged uint64)

Stats returns lifetime counters: total rounds, divergent rounds.

func (*Monitor) Stop

func (m *Monitor) Stop()

Stop halts the monitor loop.

type RPCHeadSource

type RPCHeadSource struct {
	// contains filtered or unexported fields
}

RPCHeadSource is a HeadSource backed by a Lotus-compatible JSON-RPC endpoint's Filecoin.ChainHead. It is used ONLY to corroborate or dispute the head Lantern already derived from gossip (see package doc); it is never the source of truth for the head.

Kind defaults to bootstrap.KindForest for an operator-supplied node; pass bootstrap.KindLanternGateway / KindUser explicitly to tag the project gateway or a user --peer so diversity counting is honest.

func NewRPCHeadSource

func NewRPCHeadSource(name string, kind bootstrap.Kind, url, token string, timeout time.Duration) *RPCHeadSource

NewRPCHeadSource builds an RPC-backed head source. Empty name derives one from the URL; zero timeout defaults to 15s.

func (*RPCHeadSource) HeadEpoch

func (s *RPCHeadSource) HeadEpoch(ctx context.Context) (abi.ChainEpoch, error)

func (*RPCHeadSource) Kind

func (s *RPCHeadSource) Kind() bootstrap.Kind

func (*RPCHeadSource) Name

func (s *RPCHeadSource) Name() string

type Result

type Result struct {
	Status        Status
	LocalHead     abi.ChainEpoch
	Agreeing      int             // distinct-Kind sources within Lookback
	Disagreeing   int             // distinct-Kind sources outside Lookback
	Reachable     int             // sources that answered at all
	Total         int             // sources configured
	MedianExtHead abi.ChainEpoch  // median external head (−1 if none)
	At            time.Time       // when this round completed
	PerKind       map[string]bool // Kind -> agreed (for dashboard)
}

Result is a snapshot of the most recent check round.

type Status

type Status int

Status is the outcome of one check round.

const (
	// StatusUnknown: no round has run yet.
	StatusUnknown Status = iota
	// StatusAgree: enough independent sources are within Lookback of our
	// local head. The head is corroborated.
	StatusAgree
	// StatusDiverge: a quorum of independent sources reports a head
	// outside Lookback of ours. Possible eclipse / fork-follow. ALARM.
	StatusDiverge
	// StatusInsufficient: too few sources were reachable to make a call.
	// Not an alarm by itself, but means the head is uncorroborated.
	StatusInsufficient
)

func (Status) String

func (s Status) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL