Quasar Unified Consensus Protocol
Physics-inspired consensus engine unifying Photon selection, Wave voting, Focus convergence, Prism geometry, Horizon predicates, and Flare finalization
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-305 — Nova'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:
- Fragmentation: Different engines for different chain types
- Latency: Multi-second finality unsuitable for real-time applications
- Complexity: Monolithic designs that are hard to reason about
- Adaptability: Difficult to upgrade individual components
Quasar addresses these through a modular, physics-inspired architecture where each component has a single responsibility:
| Component | Physics Metaphor | Responsibility |
|---|---|---|
| Photon | Light particle emission | Poll-committee sampling |
| Wave | Wave propagation | Opinion polling |
| Focus | Constructive interference | Confidence building |
| Prism | Light refraction | DAG geometry |
| Horizon | Event horizon | Finality boundary |
| Flare | Stellar detonation | Final 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
proposervmenvelopes (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 mapbyHeight[H]= the canonical execution commitment finalized atH.FMUST have exactly one writer: the fold of a quorum certificate that passesPrism/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 viacanonicalIDOf. 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 onC, never on the transport envelope. Two envelopes wrapping the sameCare a duplicate alias; two certificates conflict iff theirCdiffer. -
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:
- 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'dfinalizedThroughandvm.LastAcceptedbefore any signing goroutine starts. - One-per-height / lock rule: the per-height slot at
His empty, or already bound toC(idempotent re-sign). A differentCat an as-yet-undecidedHis 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 when2α − n > f.
Two supporting mechanisms make the gate airtight:
- Strict-below prune. When height
hfinalizes, the vote-once slots are pruned with the strict testk.Height < h(never≤), andφis advanced toh, 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 atHis present (refusing a conflictingC) 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.Acceptbefore 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 enforces —
consensus/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 |
|---|---|---|---|
| 3 | 3 | 0 | unanimous (f=0; CFT only — not value-safe) |
| 4 | 3 | 1 | minimal BFT committee |
| 5 | 4 | 1 | current fleet — 4-of-5 |
| 6 | 5 | 1 | |
| 7 | 5 | 2 | |
| 8 | 6 | 2 | |
| 9 | 7 | 2 | |
| 10 | 7 | 3 | |
| 11 | 8 | 3 | testnet floor |
| 21 | 15 | 6 | mainnet floor — 15, NOT 14 |
Why 21 → 15 and not 14. 14/21 = 66.67% does not strictly exceed ⅔, so a 14-vote cert FAILS
VerifyWeightedand cannot finalize. The smallest count whose equal stake strictly exceeds ⅔ of 21 is 15 (⌊2·21/3⌋+1 = 14+1 = 15). The shippedMainnetParamscarriesAlphaPreference = 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:
- The cert verifier —
VerifyWeightedrejects 3-of-5: with equal stake, 3/5 = 60% does not strictly exceed ⅔, so noQuorumCertassembles. - Config validation —
config.Parameters.Valid()returnsErrAlphaBelowBFTQuorumbecause2·3 − 5 = 1 < ⌊(5−1)/3⌋ + 1 = 2; two 3-of-5 quorums can certify conflicting blocks (the safety hole). - The node fail-closed backstop —
chains/manager.gocallsParameters.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 →VerifyWeightedfails) rather than finalize without a stake supermajority.
3.6 Reference
| Concern | Location |
|---|---|
| Strict ⅔-stake cert verifier (the α rule) | consensus/engine/chain/cert.go — QuorumCert.VerifyWeighted |
| Live-set committee sizing | node/chains/quorum.go — selectConsensusParams, localBFTParamsForN, bftSafeAlpha |
| Equal-stake α / BFT floor / overrides | consensus/config/config.go — Parameters.Valid(), ErrAlphaBelowBFTQuorum, consensus-{sample,quorum}-size |
| Epoch-anchored set read | node/chains/quorum.go — validatorSetAtHeight (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
| Metric | Value | Notes |
|---|---|---|
| Time to Finality | 400-800ms | Sub-second in normal conditions |
| Message Complexity | O(kn) | k = poll sample (illustrative 20), n validators; finality committee K and accept-quorum α are live-set sized per §3 |
| Byzantine Tolerance | f < K/3 | Standard BFT guarantee; f = K − α for the finalizing committee of size K (§3) |
| Rounds to Finality | 3-5 | Based 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:
- Parallel polling (Wave)
- Adaptive thresholds (FPC)
- Confidence shortcuts (Focus)
- 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 selectionprotocol/wave/- FPC threshold votingprotocol/wave/fpc/- FPC selector implementationprotocol/focus/- Confidence accumulationprotocol/prism/- DAG geometryprotocol/horizon/- Finality predicatesprotocol/quasar/- Unified engine
Repository: https://github.com/luxfi/consensus
Security Considerations
- VRF Security: Photon selection requires secure VRF keys
- Sample Bias: Wave requires cryptographic random sampling
- Confidence Gaming: Focus resets prevent manipulation
- DAG Attacks: Prism/Horizon detect conflicting vertices
- 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
Copyright and related rights waived via CC0.