LPsLux Proposals
Consensus Systems
LP-110

Quasar Unified Consensus Protocol

Superseded

Physics-inspired consensus engine unifying Photon selection, Wave voting, Focus convergence, Prism geometry, Horizon predicates, and Flare finalization

Category
Core
Created
2025-01-29

SUPERSEDED — finality authority (2026-07-09). The canonical Lux consensus specification is LP-305 (Nova decides · Quasar attests · Block-STM executes). This LP's component definitions (Photon / Wave / Focus / Prism / Horizon / Flare) remain valid and are referenced by LP-305, but its finality-authority model is superseded: where this LP (esp. §3) states that finality safety is the ⅔-stake quorum certificate and is "NOT pure … β finality," and where §2.7 admits a "Tendermint-style lock rule," read LP-305Nova's β-confidence sampling is the sole finality authority, and the ⅔-stake certificate is a post-accept attestation (engine/chain/attestation.go, owner decision 2026-07-09) that trails acceptance and never gates it. The cert-as-decider wiring described here is the accretion ripped out in v1.36; see the postmortem ~/work/lux/consensus/docs/postmortems/tendermint-accretion.md. This LP's normative double-finalization proof (§2.7) remains a valid safety argument for the attestation layer.

Implementation status (code-audited 2026-07-03): PARTIAL Quasar engine confirmed (consensus/protocol/quasar/engine.go:19,59); all six subprotocol packages exist under consensus/protocol/; quorum-cert logic lives in protocol/quasar/quorum_cert.go + engine/chain/quasar.go; Photon VRF committee sampling unimplemented.

Abstract

Quasar is the unified consensus protocol for Lux Network, achieving sub-second finality through a physics-inspired multi-phase architecture. The protocol combines six specialized components: Photon (VRF-weighted poll-committee sampling), Wave (FPC threshold voting), Focus (confidence accumulation), Prism (DAG geometry), Horizon (finality predicates), and Flare (cascading finalization). Quasar operates across all chain types (linear, DAG, EVM) with optional post-quantum security through BLS + Lattice dual signatures.

Finality is leaderless. No node proposes finality. A value block is final only when α distinct validators have each signed ACCEPT over the same position and their cumulative stake strictly exceeds ⅔ of the total — a fact any node can recompute. Whichever node first gathers those votes assembles the certificate; the cert is identical regardless of who assembles it (consensus/engine/chain/cert.go:41). Photon (below) samples the poll committee that drives preference and liveness; it does NOT elect a finality leader. The authoritative finality model is the weighted quorum in §3 — see LP-0013 for the leaderless invariant in one place.

Motivation

Current blockchain consensus mechanisms face critical limitations:

  1. Fragmentation: Different engines for different chain types
  2. Latency: Multi-second finality unsuitable for real-time applications
  3. Complexity: Monolithic designs that are hard to reason about
  4. Adaptability: Difficult to upgrade individual components

Quasar addresses these through a modular, physics-inspired architecture where each component has a single responsibility:

ComponentPhysics MetaphorResponsibility
PhotonLight particle emissionPoll-committee sampling
WaveWave propagationOpinion polling
FocusConstructive interferenceConfidence building
PrismLight refractionDAG geometry
HorizonEvent horizonFinality boundary
FlareStellar detonationFinal commitment

Specification

1. Protocol Overview

Quasar processes blocks through six phases:

┌─────────────────────────────────────────────────────────────────────────┐
│                        QUASAR CONSENSUS FLOW                            │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐                            │
│   │ PHOTON  │───▶│  WAVE   │───▶│  FOCUS  │                            │
│   │ Select  │    │  Vote   │    │ Converge│                            │
│   └─────────┘    └─────────┘    └────┬────┘                            │
│                                      │                                  │
│                                      ▼                                  │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐                            │
│   │  FLARE  │◀───│ HORIZON │◀───│  PRISM  │                            │
│   │ Commit  │    │ Finality│    │   DAG   │                            │
│   └─────────┘    └─────────┘    └─────────┘                            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

2. Component Specifications

2.1 Photon: Poll-Committee Sampling (LP-111)

Photon samples the per-round poll committee — the validators a node queries to learn the network's current preference. Sampling is VRF-weighted by stake and luminance (a performance metric) so that faster, higher-stake nodes are queried more often, which sharpens liveness.

Photon does NOT elect a finality leader. A poll committee is a sample for preference and liveness, not a proposer of finality. No node, sampled or not, finalizes a value block alone. Finality is the weighted quorum certificate of §3: any node that collects α distinct ACCEPT votes whose stake strictly exceeds ⅔ of the total assembles the identical cert, and finalization never hinges on one node (consensus/engine/chain/topology.go:8-16 — votes broadcast to ALL; first to collect α assembles the cert; "the proposer-freeze cannot recur"). Reading this section as single-proposer selection rebuilds the liveness single-point-of-failure the engine removed — see LP-0013.

VRF-weighted committee draw (sample, not single winner):

// Package photon draws the stake/luminance-weighted poll committee for a round.
type PhotonEngine struct {
    luminance  map[ids.NodeID]uint32  // Performance: 10-1000 lux
    vrfKeys    map[ids.NodeID][]byte
}

// SampleCommittee draws k validators for the round's preference poll, weighting
// each by VRF(sk, height) · stake · luminance. The draw biases the sample toward
// fast, high-stake nodes (liveness); it confers NO finality authority — finality
// is the §3 quorum cert, which any node assembles from α signed votes.
func (p *PhotonEngine) SampleCommittee(height uint64, k int, validators []Validator) []ids.NodeID {
    type weighted struct {
        id       ids.NodeID
        priority uint256.Int
    }
    ranked := make([]weighted, 0, len(validators))
    for _, v := range validators {
        output := vrf.Prove(v.SecretKey, height)
        priority := uint256.FromBytes(output)
        priority.Mul(priority, uint256.FromUint64(v.Stake))
        priority.Mul(priority, uint256.FromUint64(uint64(p.luminance[v.NodeID])))
        ranked = append(ranked, weighted{v.NodeID, priority})
    }
    sortByPriorityDesc(ranked)            // deterministic across honest nodes
    committee := make([]ids.NodeID, 0, k) // the top-k are this round's poll set
    for i := 0; i < k && i < len(ranked); i++ {
        committee = append(committee, ranked[i].id)
    }
    return committee
}

2.2 Wave: Threshold Voting (LP-113)

FPC (Fast Probabilistic Consensus) with phase-dependent thresholds:

Scope. The k, θ values below parameterize the per-round opinion poll (the Wave/FPC sampling mechanism, LP-0113), NOT the finality quorum. They are illustrative defaults for the large-committee sampling regime. The committee that actually finalizes a block, and its accept quorum α, are sized dynamically to the live validator set per §3 ("Committee Sizing & Weighted Quorum") and certified by the ⅔-by-stake quorum certificate — they are not a fixed constant.

// Package wave computes per-round thresholds and drives polling.
// k/θ here parameterize the opinion poll (LP-0113), not the finality quorum;
// the finalizing committee + accept-quorum α are the live-set values in §3.
type WaveEngine struct {
    k         int     // Poll sample size (illustrative default 20; see §3)
    thetaMin  float64 // Initial threshold (0.5)
    thetaMax  float64 // Final threshold (0.8)
}

// SelectThreshold picks θ ∈ [θ_min, θ_max] using PRF for phase
func (w *WaveEngine) SelectThreshold(phase uint64) float64 {
    // Sigmoid cooling: θ(r) = θ_min + (θ_max - θ_min) / (1 + e^(-r/τ))
    tau := 10.0
    sigmoid := 1.0 / (1.0 + math.Exp(-float64(phase)/tau))
    return w.thetaMin + (w.thetaMax-w.thetaMin)*sigmoid
}

// Poll executes one voting round
func (w *WaveEngine) Poll(sample []NodeID, item Decidable) (preferOK, confOK bool) {
    votes := collectVotes(sample, item)
    ratio := float64(countPositive(votes)) / float64(len(votes))
    theta := w.SelectThreshold(item.Phase())

    preferOK = ratio > theta
    confOK = ratio > theta + 0.1  // Higher bar for confidence
    return
}

2.3 Focus: Confidence Accumulation (LP-114)

Accumulates confidence through consecutive successful rounds:

// Package focus accumulates confidence by counting β consecutive successes
type FocusEngine struct {
    beta       int  // Required consecutive successes (3)
    confidence map[ids.ID]int
}

func (f *FocusEngine) RecordSuccess(itemID ids.ID, confOK bool) bool {
    if confOK {
        f.confidence[itemID]++
        if f.confidence[itemID] >= f.beta {
            return true  // Locally finalized
        }
    } else {
        f.confidence[itemID] = 0  // Reset on failure
    }
    return false
}

2.4 Prism: DAG Geometry (LP-116)

Projects the DAG into votable slices:

// Package prism provides DAG geometry: frontiers, cuts, and refractions
type PrismEngine struct {
    dag     *DAG
    cuts    map[uint64][]ids.ID  // Height -> vertex IDs
}

// Frontier returns maximal antichain (tips of the DAG)
func (p *PrismEngine) Frontier() []ids.ID {
    return p.dag.Tips()
}

// Cut selects a thin slice across causal layers
func (p *PrismEngine) Cut(height uint64) []ids.ID {
    return p.dag.VerticesAtHeight(height)
}

// Refract projects vertices into votable sub-slices
func (p *PrismEngine) Refract(vertices []ids.ID, k int) [][]ids.ID {
    // Deterministically partition into k groups
    groups := make([][]ids.ID, k)
    for i, v := range vertices {
        groups[i%k] = append(groups[i%k], v)
    }
    return groups
}

2.5 Horizon: Finality Predicates (LP-115)

Determines when vertices cross the finality boundary:

// Package horizon houses DAG order-theory predicates
type HorizonEngine struct {
    dag       *DAG
    threshold int  // 2f+1 for Byzantine tolerance
}

// Certificate detects when vertex has ≥2f+1 support
func (h *HorizonEngine) Certificate(vertexID ids.ID) bool {
    support := h.dag.SupportCount(vertexID)
    return support >= h.threshold
}

// Skip detects when vertex has ≥2f+1 opposition (will be skipped)
func (h *HorizonEngine) Skip(vertexID ids.ID) bool {
    opposition := h.dag.OppositionCount(vertexID)
    return opposition >= h.threshold
}

// Reachable checks if ancestor is reachable from descendant
func (h *HorizonEngine) Reachable(ancestor, descendant ids.ID) bool {
    return h.dag.IsAncestor(ancestor, descendant)
}

2.6 Flare: Cascading Finalization (LP-112)

Commits vertices in causal order:

// Package flare finalizes DAG cuts via cascading accept
type FlareEngine struct {
    dag      *DAG
    horizon  *HorizonEngine
    accepted map[ids.ID]bool
}

func (f *FlareEngine) Finalize(cut []ids.ID) []ids.ID {
    finalized := []ids.ID{}

    // Walk dependencies in causal order
    for _, vertexID := range topologicalSort(cut) {
        // Check all dependencies are finalized
        deps := f.dag.Dependencies(vertexID)
        allDepsFinalized := true
        for _, dep := range deps {
            if !f.accepted[dep] {
                allDepsFinalized = false
                break
            }
        }

        // Finalize if dependencies met and certificate detected
        if allDepsFinalized && f.horizon.Certificate(vertexID) {
            f.accepted[vertexID] = true
            finalized = append(finalized, vertexID)
        }
    }

    return finalized
}

2.7 Finality admission: separating acceptance from finality (Normative)

This section is normative and is the standard of record for the boundary between acceptance (which branch a node builds on) and finality (when a block becomes irreversible). It is the durable fix for a class of double-finalization faults that manifested twice in production: the 1082814 mainnet fork and the height-10 devnet storm. The formal treatment — definitions, lemmas, and the No double-finalization theorem — is proofs/quasar-cert-soundness.tex §"Finality Admission", built on proofs/definitions/finality-definitions.tex. This section is the readable companion to that proof.

Motivation — why two authorities can fight

Lux inherited the Nova/Snowman acceptance machine (Photon → Wave → Focus → Flare, §2.1–2.6) and layered a Quasar quorum-certificate finality machine beside it. Acceptance is deliberately permissive: it lets a node prefer one sibling over another, tolerates competing blocks at a height, and admits mempool- and timestamp-nondeterminism — because none of its decisions are irreversible. Quasar finality is the opposite: a height, once certified, is frozen forever.

The bug class arises when both machines are allowed to declare irreversibility. If a locally accepted or merely preferred sibling can cross the finalized boundary without a quorum certificate that extends the finalized frontier, then two nodes that locally preferred different siblings can each believe a different block is final at the same height. That is exactly what happened:

  • 1082814: two proposervm envelopes (2U2pR3D, wDMUyGy) wrapped the identical inner execution block (5DEgMudU). The engine keyed finality on the outer envelope id, so it saw a fork where there was only a duplicate alias, and nodes that had locally accepted different envelopes diverged.
  • height-10 storm: a node signed the winner at a height, finalized it, then an inclusive guard prune deleted that height's vote-once slot before a late, differently-enveloped losing sibling was rejected — so the node signed a second block at the same, already-decided height, and a second quorum certificate formed.

The cure is a single discipline: finality has exactly one writer, and a decided height can never be signed again.

Intuition — acceptance is fluid, finality is a ratchet

Picture two clocks. The acceptance clock can run backward: a node may abandon a preferred sibling and adopt another as gossip arrives — this fluidity is what lets a partitioned network re-converge. The finality clock is a ratchet: it only advances, one height at a time, and only when a quorum certificate proves the whole network agreed. Avalanche keeps the ratchet safe with a structural fact — its accepted frontier is monotone, and a decided block's siblings are pruned and become unsignable (topological.go: acceptPreferredChild + rejectTransitively).

The Lux fix is to lift that same "decided ⇒ unsignable" fact onto the sign path, and to make it durable so it survives a crash. Once a node has finalized height H, it must refuse to sign anything at height ≤ H ever again — even after an in-memory guard entry is pruned, and even after a restart. That refusal is the ratchet's pawl.

Definitions

  • Acceptance A — the local preference/accept state machine (§2.1–2.6). Fluid, sibling-tolerant, never irreversible alone.

  • Certified finality F — the map byHeight[H] = the canonical execution commitment finalized at H. F MUST have exactly one writer: the fold of a quorum certificate that passes Prism/verify (§3, §5) over the branch that extends the finalized frontier (or the bootstrap frontier-trust path).

  • Canonical commitment C — a block's inner execution identity (state / payload root), obtained via canonicalIDOf. A bare, non-wrapped block degrades to its own outer id, making the scheme inert there. Finality, equivocation, vote-locks, and the sign gate are all keyed on C, never on the transport envelope. Two envelopes wrapping the same C are a duplicate alias; two certificates conflict iff their C differ.

  • Durable decided-floor φ

    φ = max( FinHeight,                 // F's certified height; (0,⊥) until the first cert
             fsync(finalizedThrough),   // monotone height persisted in the vote-guard file at each finalize
             height(vm.LastAccepted) )  // the node's applied head, a durable lower bound
    

    φ is monotone, restart-surviving, and — crucially — a sound lower bound on the network's decided height (Lemma Floor soundness in the proof: every argument is a height the network has finalized, so φ never refuses a height the network has not decided, and therefore costs no liveness).

Construction — the decided-height sign gate

A validator's single accept-signature at height H on commitment C is admitted (reserveSlotForSign) only when both hold:

  1. Decided-height gate: H > φ. A height at or below φ is decided — one commitment was certified there — so all of its siblings are permanently unsignable. This is the ratchet pawl, and it is durable: φ is reconstructed at boot from the fsync'd finalizedThrough and vm.LastAccepted before any signing goroutine starts.
  2. One-per-height / lock rule: the per-height slot at H is empty, or already bound to C (idempotent re-sign). A different C at an as-yet-undecided H is refused outright on the legacy single-phase path; on the round-scoped view-change path it is governed by a Tendermint-style lock rule (one precommit per (H, round); migration to another value only on a proof-of-lock), which is safe when 2α − n > f.

Two supporting mechanisms make the gate airtight:

  • Strict-below prune. When height h finalizes, the vote-once slots are pruned with the strict test k.Height < h (never ), and φ is advanced to h, in one fsync'd write in the same critical section. So the just-finalized height keeps its slot until a higher height finalizes, and by then φ ≥ h+1 > h. The result is the no-window invariant: at every instant, either the slot at H is present (refusing a conflicting C) or φ ≥ H (refusing any) — the inclusive-prune gap that admitted the storm's late sibling cannot exist.
  • Fail-closed finalize → VM-accept. The finalizer calls VM.Accept before committing a block's finality and checks the error; φ advances only to the height the VM actually applied. Consensus finality therefore can never run ahead of the applied EVM head. If the VM refuses a finalized block, the chain halts at that block (safe) rather than diverging; recovery is the bounded phantom-floor reconcile (reconcile.go), which lowers φ only to at least the fleet-max applied height and is proved fork-free (the abandoned heights were never externalized and their cert material is unrecoverable post-restart).

Security argument — why double-finalization is unreachable

Suppose, for contradiction, two certificates existed at height H over different commitments C_A ≠ C_B. Both must verify against the validator set of H's epoch — the set-root is folded into every vote and cross-checked on every incoming cert, so both certificates are over the same set of total stake S. Each carries signer stake ≥ τS with τ ≥ ⅔, so by stake-weighted quorum intersection their signer sets overlap in stake ≥ (2τ − 1)S ≥ S/3. With Byzantine stake < S/3, that overlap contains an honest validator — which therefore signed both C_A and C_B at height H. But the no-window invariant and the durable floor guarantee an honest validator signs at most one commitment per height, including across restarts. Contradiction. Hence no observable double-finalization below S/3 Byzantine stake; equivalently, any observed conflicting pair is irrefutable evidence of ≥ S/3 Byzantine stake. This is proofs/quasar-cert-soundness.tex Theorem No double-finalization; the pre-fix guard silently required a "healthy sequential net" side-condition, which the storm and a rolling restart both violated — the durable floor removes that side-condition.

PART-A (the fail-safe boundary). φ is a reader. It MUST NOT write to byHeight, the certified height, or the equivocation index, and it reads only the recovery hint's height, never its identity. It can therefore only ever cause a node to refuse more signing — it can never manufacture finality. F.Height() stays (0,⊥) until the first verified cert folds. This is the incident-1082814 PART-A invariant (proofs/… Corollary PART-A non-interference).

Equivocation posture (verify-before-slash). When a certificate arrives at an already-final height with a different canonical, the node runs the full quorum-verify predicate before recording any evidence — a forged cert (junk signatures naming honest validators) slashes no one. Only a verified α-of-K cert over a different C is recorded as a DoubleVote; the node then rejects the second cert and keeps running. It does not halt the fleet — the earlier os.Exit(1) on any detected conflict was itself a self-inflicted liveness fault.

Worked example — the height-10 storm, before and after

Five validators, α = 4. A storm has multiple proposers building at height 10.

Pre-fix (inclusive prune, envelope-keyed, no durable floor). Node v signs winner A₁₀, A₁₀ gathers 4 votes and finalizes; the finalizer prunes slots ≤ 10, deleting slot{10}. A losing sibling B₁₀ — wrapped in a different outer envelope, so not in A₁₀'s rejected subtree — is still tracked and undecided. The convergence pass re-offers height 10; reserveSlotForSign(10, B) finds an empty slot and admits v's second signature. Enough nodes do the same → a second 4-of-5 cert over B₁₀ → two finalized commitments at height 10.

Fixed (strict-below prune, canonical-keyed, durable floor). When A₁₀ finalizes, the prune uses k.Height < 10, so slot{10} is retained, and φ advances to 10 in the same fsync'd write. Now B₁₀ is refused twice over: the retained slot{10} is bound to A's canonical (so a different canonical is rejected), and — even after height 11 finalizes and slot{10} is finally pruned — φ ≥ 11 > 10 makes height 10 unsignable by the decided-height gate. A restart in the middle changes nothing: φ is rebuilt from the fsync'd finalizedThrough before signing resumes. The second cert can never form. Empirically, this drove the same storm past height 10 to height 76, one canonical per height fleet-wide, zero equivocation, with a DEX 0x9999 fill at block 74 and a clean rolling-restart-mid-storm reconverge.

Reference implementation

luxfi/consensus v1.35.16 — engine/chain/engine.go: reserveSlotForSign (the gate), canonicalIDOf (inner commitment), pruneCommittedSlotsBelow (strict-below + φ advance), seedDecidedFloorFromVM (boot seed); topology.go: HandleIncomingCert / reportCertEquivocation (verify-before-slash); reconcile.go (bounded phantom-floor recovery). Regression: finalize_resign_test.go::TestFinalizeThenResign_DecidedHeightIsUnsignable (fails on the pre-fix inclusive prune, passes on the fix) and storm_convergence_test.go (safety assertion, former -race skip removed).

3. Committee Sizing & Weighted Quorum (Normative)

This section is normative and is the standard of record for how Quasar sizes the finalizing committee and its accept quorum. The phases above (Photon/Wave/ Focus/Prism/Horizon/Flare) are the per-round opinion-polling and DAG-ordering mechanism; THIS section governs finality. Block finality SAFETY comes from a ⅔-by-stake quorum certificate, not from a fixed sample size or a count of β rounds.

SUPERSEDED by LP-305 (finality authority, 2026-07-09). The engine model below — the ⅔-stake certificate as the decider — is the pre-v1.36 accretion. In canonical Lux consensus Nova's β-confidence is the finality authority and this certificate is a post-accept attestation (LP-305 §1–§2). Read the paragraph below as the description of the attestation the sampler's Accept produces, not of a second decider.

Engine model (pre-v1.36; superseded — see above). Quasar's finality layer is a ⅔-stake quorum-CERTIFICATE engine (Tendermint-style portable certificate: assemble α signed ACCEPT votes whose cumulative stake strictly exceeds ⅔ of total stake → a QuorumCert → finalize). It is NOT pure Avalanche/Snowball-with-β finality. Wave/Focus β-rounds drive preference convergence and liveness; they are not the safety boundary. A block is final only when a quorum certificate exists for it — no node, not even the proposer, finalizes a value block without one.

3.1 The committee IS the live validator set

For a given block, the committee size K = the live validator-set size, read per epoch from the validator set at a deterministic P-chain height — NOT a hardcoded constant, and NOT each node's instantaneous view of who is online. Every honest validator computes the identical (N, K, α) for a given block because the set is read at the same P-height-anchored epoch (validators.State.GetValidatorSet(height)), so the set-root, the per-voter public keys, and the ⅔-stake tally are all measured against one set.

For the native primary-network chains (P/C/D) the committee is the primary-network validator set; an L1 sizes its committee to its own staking set. The historical fixed defaults (K=20 Default, K=21 mainnet, K=11 testnet) are now lower-bound safety floors, not the operating committee: the engine derives the operating committee from the live set and the floors only reject a set too small to be Byzantine-safe for that network (mainnet K≥11, testnet K≥5).

3.2 The accept quorum α (strict > ⅔)

The accept quorum α is the minimum vote count whose cumulative STAKE strictly exceeds ⅔ of total stake. It is derived from the same rule the quorum- certificate verifier enforcesconsensus/engine/chain.QuorumCert.VerifyWeighted — which admits a cert iff:

Σ (stake of voters in the cert)  >  ⌊ 2/3 · Σ(total stake) ⌋        (STRICT)

The verifier computes the threshold overflow-safely from the total alone and requires voted > twoThirdsFloor (Tendermint +⅔). There is no duplicated quorum math anywhere in the spec or the code: α is whatever satisfies the verifier's strict-stake predicate. The check MUST be strict >, never .

For the common equal-stake case (each validator weight = 1, total = N) this reduces to a closed form:

α = ⌊ 2N/3 ⌋ + 1
f = N − α  ≈  N/3        (Byzantine / non-responsive validators tolerated)

Strict > ⅔ committee table (the correctness check — STRICT, not ≥):

Nα (= ⌊2N/3⌋+1)f = N−αnote
330unanimous (f=0; CFT only — not value-safe)
431minimal BFT committee
541current fleet — 4-of-5
651
752
862
972
1073
1183testnet floor
21156mainnet floor — 15, NOT 14

Why 21 → 15 and not 14. 14/21 = 66.67% does not strictly exceed ⅔, so a 14-vote cert FAILS VerifyWeighted and cannot finalize. The smallest count whose equal stake strictly exceeds ⅔ of 21 is 15 (⌊2·21/3⌋+1 = 14+1 = 15). The shipped MainnetParams carries AlphaPreference = 15 — spec and config agree. Any "α = 14 for N = 21" claim is stale and MUST be read as 15.

Note that the engine's BFT-overlap floor in config.Parameters.Valid() (2·α − K ≥ ⌊(K−1)/3⌋ + 1, surfaced as bftSafeAlpha/ErrAlphaBelowBFTQuorum) is a lower bound on α. For equal stake the binding constraint is always the stricter of the overlap floor and the strict-stake count ⌊2N/3⌋+1; the strict-stake count is the operative α. (They coincide for most N and differ only at boundary N where the overlap floor is laxer, e.g. N=21: overlap floor 14, strict-stake 15 → α = 15.)

3.3 Epoch-anchored sizing and the two-sided clamp

(N, K, α) is anchored to the validator set at the block's P-chain epoch height. At an epoch boundary (a validator joins or leaves), the boundary block MUST satisfy BOTH the old quorum (over the pre-change set) and the new quorum (over the post-change set). This two-sided clamp means:

  • No fork: a cert valid under the old set and a cert valid under the new set both pin to the boundary's ValidatorSetRoot; a cert gathered under one epoch's set cannot be laundered into the other.
  • No stall: because each validator's view of K shifts by at most ±0/1 across a single join/leave, α moves smoothly and the boundary block stays reachable under both sets — the adaptation is continuous, not a step that wedges finality.

The set-root binding (VotePosition.ValidatorSetRoot, folded into the canonical signed vote message) makes "⅔-by-stake measured at the cert-position epoch" an enforced cryptographic invariant, not an assumption: a cross-epoch stake change cannot retroactively flip an already-correct cert.

3.4 Current fleet: K = 5 → α = 4

With 5 validators per network, this yields K = 5, α = 4 (a 4-of-5 quorum, tolerating f = 1 laggard or Byzantine validator) on devnet, testnet, and mainnet. This is the fastest setting that finalizes a 5-validator set: it is the smallest α that strictly clears ⅔ of stake while leaving real liveness slack (the proposer polls all 4 peers and one may lag). It recomputes automatically as validators join or leave — at 6 validators it becomes K = 6, α = 5; at 21 it becomes K = 21, α = 15; and so on, with no config change.

3.5 Floor — never weaken α below the cert threshold

α MUST NEVER be set below the certificate threshold; nothing may bypass VerifyWeighted; the ⅔ stake fraction MUST NEVER be weakened. Concretely, a 3-of-5 quorum (α = 3 on K = 5) is non-finalizing and is rejected at three independent layers — do not "optimize" α downward:

  1. The cert verifierVerifyWeighted rejects 3-of-5: with equal stake, 3/5 = 60% does not strictly exceed ⅔, so no QuorumCert assembles.
  2. Config validationconfig.Parameters.Valid() returns ErrAlphaBelowBFTQuorum because 2·3 − 5 = 1 < ⌊(5−1)/3⌋ + 1 = 2; two 3-of-5 quorums can certify conflicting blocks (the safety hole).
  3. The node fail-closed backstopchains/manager.go calls Parameters.ValidateForValueNetwork(networkID) for every multi-node value chain and refuses to start the chain ("refusing to start multi-node chain … with non-BFT consensus params") rather than run an unsafe quorum. A K>1 chain with no published height-indexed stake state also fails closed (tally 0 → VerifyWeighted fails) rather than finalize without a stake supermajority.

3.6 Reference

ConcernLocation
Strict ⅔-stake cert verifier (the α rule)consensus/engine/chain/cert.goQuorumCert.VerifyWeighted
Live-set committee sizingnode/chains/quorum.goselectConsensusParams, localBFTParamsForN, bftSafeAlpha
Equal-stake α / BFT floor / overridesconsensus/config/config.goParameters.Valid(), ErrAlphaBelowBFTQuorum, consensus-{sample,quorum}-size
Epoch-anchored set readnode/chains/quorum.govalidatorSetAtHeight (membership, pubkey, set-root, stake all keyed off the P-height epoch)

Implementation status. The dynamic live-set sizing ships in node v1.30.47 (in-flight). The ⅔-by-stake quorum certificate (VerifyWeighted, set-root epoch binding) is already the finality authority. This section is the spec of record regardless of rollout state.

4. Unified Consensus Loop

The Quasar engine orchestrates all components:

type QuasarEngine struct {
    photon  *PhotonEngine
    wave    *WaveEngine
    focus   *FocusEngine
    prism   *PrismEngine
    horizon *HorizonEngine
    flare   *FlareEngine
}

func (q *QuasarEngine) ProcessRound(height uint64) {
    // 1. PHOTON: Sample the round's poll committee (preference/liveness — NOT a
    //    finality leader; finality is the §3 quorum cert any node can assemble).
    committee := q.photon.SampleCommittee(height, q.wave.k, q.validators)

    // 2. Receive gossiped blocks/vertices (broadcast to all; no single proposer)
    items := receiveGossip()

    // 3. PRISM: Structure DAG
    cut := q.prism.Cut(height)
    slices := q.prism.Refract(cut, q.wave.k)

    // 4. WAVE: Poll each slice against the sampled committee
    for _, slice := range slices {
        sample := committee
        for _, item := range slice {
            preferOK, confOK := q.wave.Poll(sample, item)

            // 5. FOCUS: Accumulate confidence
            if q.focus.RecordSuccess(item.ID(), confOK) {
                // 6. HORIZON: Check finality predicates
                if q.horizon.Certificate(item.ID()) {
                    // 7. FLARE: Commit
                    q.flare.Finalize([]ids.ID{item.ID()})
                }
            }
        }
    }
}

5. Performance Characteristics

MetricValueNotes
Time to Finality400-800msSub-second in normal conditions
Message ComplexityO(kn)k = poll sample (illustrative 20), n validators; finality committee K and accept-quorum α are live-set sized per §3
Byzantine Tolerancef < K/3Standard BFT guarantee; f = K − α for the finalizing committee of size K (§3)
Rounds to Finality3-5Based on β confidence; finality requires a ⅔-by-stake quorum certificate (§3)

6. Post-Quantum Extension

Quasar supports optional quantum-safe signatures:

type QuasarPQ struct {
    *QuasarEngine

    // Round 1: BLS aggregate (fast, classical)
    blsSignatures map[ids.ID][]byte

    // Round 2: Lattice (quantum-safe, larger)
    latticeSignatures map[ids.ID][]byte
}

func (q *QuasarPQ) FinalizeWithPQ(itemID ids.ID) {
    // Require both signature types
    if len(q.blsSignatures[itemID]) > 0 &&
       len(q.latticeSignatures[itemID]) > 0 {
        q.flare.Finalize([]ids.ID{itemID})
    }
}

Rationale

Modular Design

Each component handles one concern:

  • Easier to reason about correctness
  • Components can be upgraded independently
  • Clear interfaces enable testing

Physics Metaphors

The naming provides intuition:

  • Light (photon) → who speaks
  • Wave → how opinions propagate
  • Focus → convergence point
  • Prism → structure/refraction
  • Horizon → boundary of finality
  • Flare → explosive commitment

Sub-Second Finality

Achieved through:

  1. Parallel polling (Wave)
  2. Adaptive thresholds (FPC)
  3. Confidence shortcuts (Focus)
  4. Certificate detection (Horizon)

Backwards Compatibility

Quasar maintains compatibility with legacy linear-chain consensus (historical Snowman, now Nova mode per LP-134) through interface adapters:

type LegacyChainAdapter struct {
    quasar *QuasarEngine
}

func (s *LegacyChainAdapter) RecordPoll(votes ids.Bag) {
    // Convert legacy linear-chain votes (historical Snowman) to Quasar polling
    s.quasar.wave.ProcessVotes(votes)
}

Test Cases

See component-specific LPs for detailed test cases:

  • LP-111: Photon selection tests
  • LP-112: Flare finalization tests
  • LP-113: Wave/FPC voting tests
  • LP-114: Focus convergence tests
  • LP-115: Horizon predicate tests
  • LP-116: Prism geometry tests

Reference Implementation

Primary Location: ~/work/lux/consensus/

Component Directories:

  • protocol/photon/ - VRF-based proposer selection
  • protocol/wave/ - FPC threshold voting
  • protocol/wave/fpc/ - FPC selector implementation
  • protocol/focus/ - Confidence accumulation
  • protocol/prism/ - DAG geometry
  • protocol/horizon/ - Finality predicates
  • protocol/quasar/ - Unified engine

Repository: https://github.com/luxfi/consensus

Security Considerations

  1. VRF Security: Photon selection requires secure VRF keys
  2. Sample Bias: Wave requires cryptographic random sampling
  3. Confidence Gaming: Focus resets prevent manipulation
  4. DAG Attacks: Prism/Horizon detect conflicting vertices
  5. Finality Safety: Flare only commits certified vertices

References

[1] Quasar Consensus Protocol Specification. 2024. [2] Micali, S., et al. "Verifiable Random Functions". FOCS 1999. [3] Popov, S., et al. "FPC-BI: Fast Probabilistic Consensus". 2021. [4] Team Rocket. "Snowflake to Avalanche: A Novel Metastable Consensus Protocol Family". 2018. (Historical prior art; Lux's Quasar consensus family supersedes the Snow* family — see LP-020, LP-134.)

Copyright and related rights waived via CC0.