handleVote routes a vote for a block that is no longer pending but IS in
finalizedByCert to attestFinalizedVote, which resolves the signed position through
lookupAcceptedPos. applyBranchFinalization sets finalizedByCert and drops the pending
block; rememberAcceptedPos ran later still, inside promoteQuasar. Between those two
points the tap was OPEN and the position ABSENT, so a vote arriving there took the
finalized path, missed the lookup, and returned in silence.
That vote is not recoverable. Accept votes are solicited once and promoteQuasar does no
retroactive promotion, so losing the 2/3-th one leaves the height Nova-only PERMANENTLY:
settlement never exports it, and FinalityStatus reports Degraded with a responsive stake
below the truth — a fleet with 4 of 5 up and all four voting reads back as 0.6 on a live
consumer surface. The window widens under CPU contention, which is where it surfaced, as
the source of the export-liveness flake fixed in f84579ed.
The write moves to before applyBranchFinalization, so the record exists before anything
can observe the block as finalized, and leaves promoteQuasar with one job. A later
VM.Accept failure cannot make it lie: attestFinalizedVote is reachable only for a block
finalizedByCert actually holds.
TestAcceptedPos_RememberedBeforeFinalityIsVisible pins the ordering without needing a
race — the VM's Accept runs inside the same finalization step, so probing lookupAccepted
Pos from Accept observes exactly the interval that used to be empty. It FAILS with the
write moved back.
Also in this commit, from the same review:
- The no-cert-at-height branch gets its own log at Debug. It was sharing the
"unreadable" line and reporting errNoCertAtHeight in a field named getBlockError,
which is self-contradictory, and firing at Error on what is the ORDINARY case: the
steer target is normally the build tip, which sits above the frontier by definition.
- The FourOfFive gate keeps a direct quasarTip() read. Asserting only at blk's height
let a frontier whose canonical nobody certified pass, and that shape is producible:
SyncQuasarFrontier is a SECOND writer that seeds the frontier from the VM's durable
export record with no Nova-ledger check and accepts an empty canonical. The gate now
asserts chain membership AND no conflicting canonical at blk's height.
- waitForExportFrontier advanced its height cursor while leaving parentID behind on a
failed convergence, so every later iteration built on a stale parent and a real
export stall was reported as unconvergeable noise. It now stops at the first
non-converging height.
- Corrected in the comments: the Nova cert freezes at NovaQuorum(5)=3, and alpha does
not gate cert assembly at all. f84579ed's message said "the bare alpha=4 without 2/3",
which is arithmetically impossible — prodParams5 sets alpha=4 and twoThirdsCount(5)
is also 4 — and sends the next reader down a dead end.
Co-authored-by: Hanzo Dev <[email protected]>
Lux Consensus
Lux is not merely adding post-quantum signatures to a chain; it defines a hybrid finality architecture for DAG-native consensus, with protocol-agnostic threshold lifecycle, post-quantum threshold sealing, and cross-chain propagation of Horizon finality.
See LP-105 §Claims and evidence for the canonical claims/evidence table and the ten architectural commitments — single source of truth.
Quasar Family of Consensus
Quasar is the consensus engine for the Lux network. It provides unified consensus for linear chains (P-Chain, C-Chain) and DAG chains (X-Chain) with post-quantum finality via three independent cryptographic signing paths.
Sub-Protocols
| Protocol | Role | Package |
|---|---|---|
| Photon | K-of-N committee selection via Fisher-Yates sampling with luminance-weighted reputation | protocol/photon |
| Wave | Per-round threshold voting with FPC (Fast Probabilistic Consensus) | protocol/wave |
| Focus | Confidence accumulation: beta consecutive successes = local finality | protocol/focus |
| Nova | Linear chain consensus mode (P-Chain, C-Chain) -- wraps Ray | protocol/nova |
| Nebula | DAG consensus mode (X-Chain) -- wraps Field | protocol/nebula |
| Prism | DAG geometry: frontiers, cuts, uniform peer sampling | protocol/prism |
| Horizon | DAG order-theory: reachability, LCA, transitive closure, skip lists | protocol/horizon |
| Flare | DAG certificate/skip detection via 2f+1 quorum | protocol/flare |
| Ray | Linear chain finality driver: Wave + Focus + Sink | protocol/ray |
| Field | DAG finality driver: Wave + safe-prefix commit | protocol/field |
| Quasar | BLS + Pulsar + ML-DSA threshold signing, epoch management | protocol/quasar |
Signing Modes
Each cryptographic layer is independently toggleable:
| Mode | Layers | Use Case |
|---|---|---|
| BLS-only | BLS12-381 threshold | Fastest classical consensus |
| BLS + ML-DSA | BLS + FIPS 204 ML-DSA-65 | Dual with PQ identity proof |
| BLS + Pulsar | BLS + Ring-LWE 2-round threshold | Dual with PQ threshold proof |
| BLS + Pulsar + ML-DSA | All three in parallel | Full Quasar (triple mode) |
IsTripleMode() returns true when all three paths are configured.
TripleSignRound1 runs BLS + Pulsar + ML-DSA signing in parallel goroutines.
PQ Mode Selection (Configurable)
The active mode is selectable at runtime via config.PQMode or the
CONSENSUS_PQ_MODE env var. Defined in config/pq_mode.go:
| Mode | Env value | Maps to wire policy |
|---|---|---|
BLSOnly |
bls |
PolicyQuorum |
BLSPlusMLDSA |
bls-mldsa |
PolicyPQ |
BLSPlusCorona |
bls-rt |
PolicyPZ |
BLSPlusGroth16 |
bls-z |
PolicyQuantum (placeholder) |
TripleQuantum |
triple |
PolicyQuantum |
Run bench/pq_modes_bench_test.go for measured per-mode signing /
aggregation / verify costs and cert sizes. Storage and cert size
trade-offs:
| Mode | Sign | Agg | Verify | Cert | Storage 10K |
|---|---|---|---|---|---|
| bls | 312 µs | 8.6 ms | 714 µs | 123 B | 1.17 MB |
| bls-mldsa | 369 µs | 8.5 ms | 3.4 ms | 69 KB | 665 MB |
| bls-rt | 39 ms | 3.3 s | 1.6 ms | 33 KB | 318 MB |
| triple | 40 ms | 3.3 s | 4.3 ms | 102 KB | 981 MB |
Pulsar / triple include the full 2-round Pulsar threshold protocol
(github.com/luxfi/pulsar/threshold — Pulsar is Lux's variant with
DKG2 and Pulsar-SHA3 hash suite). Production parameters M=8, N=7,
LogN=8 (ring degree 256), Q=2^48-ish: classical 2^142, quantum 2^130.
Cert-size floor of ~33 KB is set by the (C, Z, Delta) ring polynomials;
protocol/quasar/cert_size_compare_test.go measures and verifies this.
Architecture
consensus/
protocol/
photon/ Committee selection (Emitter + Luminance)
wave/ Threshold voting + FPC
fpc/ Fast Probabilistic Consensus selector
focus/ Confidence counter (beta tracker)
prism/ DAG cuts, frontiers, uniform sampling
horizon/ DAG reachability, LCA, skip lists
flare/ DAG cert/skip classification (2f+1)
ray/ Linear chain driver (Nova uses this)
field/ DAG finality driver (Nebula uses this)
nova/ Linear chain consensus mode
nebula/ DAG consensus mode
chain/ Block interface primitives
quasar/ BLS + Pulsar + ML-DSA threshold signing
engine/ Consensus engine (Chain, DAG, PQ wrappers)
core/ Core types, DAG structures
types/ Block, Vote, Config, Bag
config/ Parameter presets (single, local, testnet, mainnet)
runtime/ VM wiring (chain IDs, validators, logging)
pkg/wire/ Wire protocol credentials (ML-DSA-44/65/87 + BLS + Ed25519)
bench/ Benchmarks
version/ Re-exports github.com/luxfi/version
Consensus Flow
Linear Chains (Nova)
Photon (select committee) -> Wave (threshold vote) -> Focus (count beta) -> Ray (decide) -> Sink
DAG Chains (Nebula)
Photon (select committee) -> Wave (threshold vote per frontier vertex)
-> Flare (cert/skip) -> Horizon (safe prefix) -> Field (commit ordered prefix) -> Committer
Quasar PQ Finality
After consensus decision, the Quasar signing layer produces threshold certificates:
- BLS: single-round threshold share via
crypto/threshold - Pulsar: 2-round Ring-LWE threshold via
luxfi/corona/threshold - ML-DSA-65: single-round FIPS 204 identity signature
All three run in parallel. A block is quantum-final when all configured certificate layers are valid.
Key Hierarchy
| Layer | Key Type | Purpose |
|---|---|---|
| Node-ID | Ed25519 | P2P transport auth |
| Validator-BLS | BLS12-381 | Classical finality votes |
| Validator-PQ | Pulsar (Ring-LWE) | PQ finality shares |
| Validator-ID | ML-DSA-65 (FIPS 204) | PQ identity attestation |
| Wallet (EVM) | secp256k1 | User transaction signatures |
| Wallet (X-Chain) | secp256k1 | UTXO locking |
Quick Start
import "github.com/luxfi/consensus"
chain := consensus.NewChain(consensus.DefaultConfig())
ctx := context.Background()
if err := chain.Start(ctx); err != nil {
log.Fatal(err)
}
defer chain.Stop()
block := &consensus.Block{
ID: consensus.NewID(),
ParentID: consensus.GenesisID,
Height: 1,
Payload: []byte("Hello, Lux!"),
}
if err := chain.Add(ctx, block); err != nil {
log.Fatal(err)
}
Configuration
// Auto-configure based on validator count
params := consensus.GetConfig(21) // mainnet defaults
// Or use presets
consensus.SingleValidatorParams() // 1 node, dev
consensus.LocalParams() // 5 nodes, local
consensus.TestnetParams() // 11 nodes, testnet
consensus.MainnetParams() // 21 nodes, production
Benchmarks
Measured on Apple M1 Max:
| Component | Operation | Time/Op |
|---|---|---|
| Wave | Vote round | 3.38 us |
| Photon | K-of-N selection | 3.03 us |
| Luminance | Brightness update | 72 ns |
ZAP wire protocol (measured, bench/):
| Configuration | Throughput |
|---|---|
| Single connection | 114K TPS |
| 20 parallel connections | 376K TPS |
| 50 conns + batch 1000 | 20.26M TPS |
Testing
GOWORK=off go test ./...
GOWORK=off go test -bench=. ./bench/
Security Model
- Pre-quantum: Attacker must corrupt >= 1/3 stake to fork
- Q-day (BLS broken): Lattice certificates prevent unsafe finalization
- Post-quantum: Security rests on Module-LWE (Pulsar) + Module-LWE/SIS (ML-DSA)
License
BSD 3-Clause. See LICENSE.