Files
consensus/LLM.md
T
zeekayandHanzo Dev 731dc22f23 cert: a floor is read once, where it is authoritative
Red found no way past the predicate — every refusal was fail-closed. What it
found instead was the opposite failure, three times over: a number about a quorum
read somewhere that is not the authority for it.

THE DOORS IN FRONT OF THE GATE. HandleIncomingCert and VerifyCatchupCertificate
each carried a cheap sub-quorum filter over the sample committee — NovaQuorum(K)
for Nova, Alpha() for Quasar — in front of verifyCert. That is a THIRD spelling of
a quantity with one definition, and it is a different quantity: the gate derives
the accept rung's floor from the SIGNING SET, where NovaSignerFloor saturates at
three, while the door read the committee's majority, which grows with K. At six
seats the gate accepts a certificate declaring three and the door demanded four;
at nine it demanded five; at eleven, six. And where the set is SMALLER than the
committee the door asked the set for more than the set's own floor: three signers
derive two, and a door reading a committee of four asked for three.

So the exact bytes a node's own assembler produces never reached the predicate
that would have admitted them. A door in front of a gate may only refuse what the
gate refuses, and these could not, so they are gone. Nothing is lost by not asking
twice: the gate demands EQUALITY with the derived floor, which is strictly
stronger than the "at least" a door applied. Nor is any work saved by refusing
early — the certificates a floor filter drops are the ones carrying FEW votes,
which are the cheap ones, and Verify stops at the first signature that fails.

THE CALLER'S ALPHA. BuildVerifiedQuorumCert took the floor as an ARGUMENT, and on
the count-only road nothing re-derived it: one vote and an alpha of one minted an
authority token on a chain of five, while a certificate carrying those exact bytes
was refused on arrival. The docstring said an alpha that disagrees with the set
yields no token, and that was true only of the weighted road. So there is no
argument. The floor is SignerFloor(tier, SignerCount(epoch)), read at the door,
and there is nothing left for a caller to disagree with.

With that, a nil stake source is refused at BOTH rungs rather than at the export
rung alone: a floor is a property of the set, and with no set there is no floor and
nothing to mint. exportNeedsStake goes with it — one rule where there were two, and
the rule Rust and C++ already have by their types, each taking a set by reference
so the absent case is unrepresentable rather than checked. The equal-stake road
keeps minting through the engine, whose committee is its own, and keeps ADMITTING
on the arrival road, where signatures have already arrived to be weighed.

THE WIDTH. SignerFloor and Quorum returned uint32 while counting a set held in an
int, so the floor wrapped at about six billion signers and SignerFloor(Quasar, ~6.4e9)
answered 1 — a floor a lone signer meets. They return int now and are narrowed only
where a certificate is written, which is one place; and because the derived clause
compares at full width, a mint that narrowed is caught by it rather than believed.

THE LAST SECOND SPELLING. attestation.go's export trigger said config.TwoThirdsCount(n)
where it means SignerFloor(Quasar, n). Same number, and that is the point: a trigger
spelled apart from the rule it must never outrun is a trigger that can drift from it.

TestTheDoorAdmitsWhatTheGateAdmits walks the four sizes where the two numbers part,
on both arrival roads, and fails on the old filter at every one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-02 02:56:02 -07:00

43 KiB
Raw Blame History

Lux Consensus -- Agent Knowledge Base

Repository: github.com/luxfi/consensus Latest Tag: v1.26.0 Go: 1.26.1

Quasar Family of Consensus

The consensus system provides two modes (linear and DAG) with optional post-quantum finality. All sub-protocols live in protocol/.

Sub-Protocols (protocol/)

Package Role Key Types
photon K-of-N committee selection, luminance tracking Emitter, Luminance
wave Threshold voting + FPC Wave[T], Config, Photon[T]
wave/fpc Fast Probabilistic Consensus selector Selector
focus Beta consecutive successes counter Tracker[ID], Confidence[ID], WindowedConfidence[ID]
prism DAG geometry: cuts, frontiers, uniform sampling Cut[T], Engine, Proposal
horizon DAG reachability, LCA, transitive closure, skip lists TransitiveClosure, LowestCommonAncestor, SkipList
flare DAG cert/skip via 2f+1 quorum Flare, HasCertificate, HasSkip
ray Linear chain finality driver Driver[T], Source[T], Sink[T]
field DAG finality driver with safe-prefix commit Driver[V], Store[V], Proposer[V], Committer[V]
nova Linear chain mode (wraps ray) Nova[T]
nebula DAG mode (wraps field) Nebula[V]
chain Block interface primitives Block, ChainState
quasar BLS + Pulsar + ML-DSA threshold signing signer, BLS, EpochManager, BundleSigner

Consensus Flow

Linear (Nova): Photon -> Wave -> Focus -> Ray -> Sink

ConsensusCert envelope (CANONICAL — supersedes the Groth16 QuasarCert narrative below)

The finality certificate is threshold CERTIFICATION, not threshold signing. One policy-gated envelope (protocol/quasar/consensus_cert.go, VerifyConsensusCert), released at v1.26.0. One-sentence spec: policy defines the required legs; cert bytes provide per-leg evidence; each mode proves the same weighted-quorum predicate over the same domain-separated finality message M. Required legs are policy-derived (ConsensusCertPolicy.RequiredLegs) and recomputed/checked against the cert's RequiredLegsRoot — never trusted from cert bytes (the anti-downgrade invariant).

Four typed EvidenceKinds (compact_evidence.go) are the chain-facing vocabulary; each decomposes onto two orthogonal axes, LegKind (WHAT is required) × EvidenceMode (HOW it is proven):

EvidenceKind wire LegKind EvidenceMode
EvidenceBeamBLS beam-bls LegClassical EvidenceClassicalAggregate
EvidencePulsarThresholdMLDSA pulsar-threshold-mldsa LegPulsarMLDSA EvidenceThresholdSig
EvidenceCoronaRingtail corona-ringtail LegCoronaLattice EvidenceThresholdSig
EvidenceP3QMLDSARollup p3q-mldsa-rollup (fallback) LegPulsarMLDSA EvidenceP3QRollup

Pulsar and P3Q satisfy the SAME leg kind (Module-LWE ML-DSA finality) by different mechanisms — the policy-table "OR". The two enums are LegKind {LegPulsarMLDSA, LegCoronaLattice, LegMagnetarSLHDSA, LegClassical} and EvidenceMode {EvidenceThresholdSig, EvidenceWeightedSigSet, EvidenceStarkCompressedSigSet, EvidenceClassicalAggregate, EvidenceP3QRollup}. So there are four typed EvidenceKinds plus the LegMagnetarSLHDSA leg (hash-based FIPS-205, Polaris profile) outside this vocabulary — production-forbidden from EvidenceThresholdSig (no aggregatable threshold structure). Never "four legs total."

Policy table (cert_policy_table.go) — exactly four operator postures:

posture string required legs
PolicyBLSFast BLS_FAST Beam
PolicyHybridPQCheckpoint HYBRID_PQ_CHECKPOINT Beam ∧ (Pulsar OR P3Q)
PolicyStrictQuasar STRICT_QUASAR Beam ∧ Pulsar ∧ Corona — full dual-lattice AND
PolicyRecoveryMode RECOVERY_MODE Beam ∧ P3Q-rollup

AND-enforcement: VerifyConsensusCert rejects if any required leg is missing, mode-disallowed, or fails its verifier, plus a cardinality check (each required leg seen exactly once). STRICT permits ONLY the compact Pulsar threshold sig for the PQ leg; RECOVERY permits ONLY P3Q; HYBRID permits either.

KeyEra (key_era.go, commit 3153aea93 — decomplect PulsarKeyEra → KeyEra, value-not-place): ONE generic KeyEra type pins one compact group key to one validator-set era, qualified by its SchemeID (ML-DSA vs Ringtail) — one type for both lanes, not a Pulsar-specific era plus a Corona one. It serves the two threshold/group-key lanes — Pulsar and Corona — ONLY; Beam (classical aggregate) and P3Q (per-validator rollup) are explicitly refused by eraSuite() (not compact group-key threshold lanes).

P3Q is FALLBACK only, bound to the committed validator set (commit 73c280efe closed a zero-stake forge): verifyP3QDirect commits the rollup root to the raw MLDSACertSet, then verifies it through the audited weighted-sig-set verifier — binding every leaf to validators.Root() by weighted-Merkle membership at the BFT weight floor. In the normal/STRICT path Pulsar compresses the whole quorum to ONE threshold signature, so the P3Q rollup of N independent sigs is the migration / recovery / bridge / audit object, never the primary finality object; STRICT structurally refuses it.

Compact — O(1), not O(N). Beam / Pulsar / Corona are each ONE O(1) aggregate signature, so the cert footprint is byte-invariant in committee size (TestCompact_O1NotON_For1000Validators asserts naive/compact ≥ 100×). At N=1000 (ML-DSA-65) that test MEASURES/logs 957× — a 3466-byte compact Beam+Pulsar checkpoint vs a 3,320,000-byte naive 1000-raw-ML-DSA object. 957× is the measured figure; the only asserted bound is ≥100×.

Boring verifier. VerifyConsensusCert is deliberately straight-line: one leg at a time, typed rejects, registry-mediated suite dispatch (no suite string can reach the wrong lane's verifier), no clever shortcuts. It is NOT constant-time — it early-returns on the first failing leg.

Decomplect. The dealerless PQ core (Pulsar / Corona, via luxfi/threshold) imports NEITHER luxfi/mpc NOR luxfi/tee — those are optional, cross-repo extensions (absent from this module's go.mod). Attestation verification is the separate luxfi/cc leaf with orthogonal vendor Kinds.

The MLDSAProof/Groth16 3-tuple below is the SUPERSEDED prior design.

Quasar Certificate (superseded — historical)

QuasarCert (see protocol/quasar/types.go:40) is a 3-tuple:

type QuasarCert struct {
    BLS        []byte  // BLS12-381 aggregate, 48 bytes classical fast path
    Pulsar   []byte  // Ring-LWE threshold (PQ), O(1) after DKG
    MLDSAProof []byte  // Z-Chain Groth16 rolling up N × ML-DSA identity sigs, ~192 bytes
    Epoch      uint64
    Finality   time.Time
    Validators int
}
Layer Scheme Hardness Raw Size In Cert
BLS BLS12-381 aggregate co-CDH 48 B 48 B
ML-DSA ML-DSA-65 (FIPS 204) Module-LWE + MSIS ~3309 B per validator 192 B (Groth16)
Pulsar Ring-LWE threshold Module-LWE O(1) after DKG variable

Modes (each layer independently toggleable):

  • BLS-only: classical fast path
  • BLS + Pulsar: dual PQ
  • BLS + Pulsar + ML-DSA: full Quasar (TripleSignRound1)
  • Full Quasar + Z-Chain ZKP: production mode (succinct certificate)

IsTripleMode() checks all three signing layers. Crypto: luxfi/crypto/bls, luxfi/crypto/mldsa, luxfi/corona/threshold.

PQ Mode Selection

config/pq_mode.go defines the configurable PQ mode enum, selectable via the CONSENSUS_PQ_MODE env var or Parameters.PQMode field:

Mode Value Description
BLSOnly bls Classical fast path, smallest cert
BLSPlusMLDSA bls-mldsa BLS + per-validator ML-DSA-65
BLSPlusCorona bls-rt BLS + Pulsar 2-round threshold
BLSPlusGroth16 bls-z BLS + Z-Chain Groth16 rollup (placeholder)
TripleQuantum triple All three layers active

engine/pq.NewConsensus resolves the mode via config.PQModeFromEnv and exposes PQMode() getter. bench/pq_modes_bench_test.go covers all modes with real signing (real BLS aggregate, real ML-DSA-65, real Pulsar 2-round threshold).

Bench (Apple M1, n=21):

Mode Sign Agg Verify Cert Storage 10K
bls 312µs 8.6ms 714µs 123 B 1.17 MB
bls-mldsa 369µs 8.5ms 3.4ms 69 KB 665 MB
bls-rt 39ms 3.3s 1.6ms 33 KB 318 MB

The Pulsar layer is implemented by Pulsar (github.com/luxfi/pulsar/threshold) — Lux's variant with DKG2 (pulsar/dkg2/) and Pulsar-SHA3 hash suite (pulsar/hash/sp800_185.go, KMAC over cSHAKE256). Pulsar params are byte-identical to the original Pulsar: M=8, N=7, LogN=8 (ring degree 256), Q=0x1000000004A01 (48-bit NTT-friendly prime), Dbar=48, Kappa=23.

Cert-size honest accounting (production params, classical 2^142 / quantum 2^130 security):

  • Signature = (C: 1 ring.Poly) + (Z: Vector[ring.Poly] len 8) + (Delta: Vector[ring.Poly] len 8) = 17 polys × 256 coeffs × 8 bytes raw ≈ 34816 B; measured 33052 B native binary, 33221 B gob (gob bloat is only 1.01x — see cert_size_compare_test.go). Native encoder ships in protocol/quasar/corona_gob.go (replaces gob).
  • 10K certs ≈ 315 MB native, 317 MB gob.
  • The earlier "10 MB / 10K = 1 KB / cert" claim was a different parameter sweep (smaller ring + smaller Q) — would lose ~20 bits classical and ~15 bits quantum security. Not interchangeable with Pulsar production. | triple | 40ms | 3.3s | 4.3ms | 102 KB | 981 MB |

Chain Separation for Threshold Cryptography

Per LP-134: T-Chain MPC and FHE roles (originally per LP-7330) are split into M-Chain and F-Chain. T-Chain is reserved for teleportvm (LP-6332).

Quasar consensus lives here in consensus/, but the threshold-crypto ceremonies that feed it are split across the primary network chains:

Chain Role
X-Chain Verifies already-signed UTXOs via Fx plugins (secp256k1fx, mldsafx, slhdsafx, ed25519fx, secp256r1fx...). Does not run MPC ceremonies.
Q-Chain Runs the Pulsar 2-round threshold for consensus only (this repo's protocol/quasar/ emits those rounds). Not a general MPC host.
M-Chain (was T-Chain MPC per LP-7330; superseded by LP-134) Runs MPC ceremonies (CGGMP21, FROST, Pulsar-general) for bridge custody of external wallets.
F-Chain (was T-Chain FHE per LP-7330; superseded by LP-134) Runs TFHE bootstrap-key generation and FHE compute (encrypted EVM).
T-Chain Now reserved for teleportvm (LP-6332): unified bridge + relay + oracle.
Z-Chain Rolls N per-validator ML-DSA identity sigs into a single 192-byte Groth16 proof per epoch (the MLDSAProof field).

Why MLDSAProof and not ThresholdMLDSA: threshold ML-DSA has no FIPS standard; research constructions hit a rejection-sampling circular dependency (see ~/work/lux/proofs/quasar-cert-soundness.tex App. A). Quasar takes the non-threshold path — each validator signs individually, Z-Chain compresses via Groth16 over BLS12-381.

Formal Proofs (LP-105 + Proof Sketch)

The paper + proof sketch carry the soundness/liveness/PQ-safety arguments:

  • ~/work/lux/papers/lp-105-quasar-consensus.tex:
    • §5 Chain Separation
    • §6 QuasarCert Formal Definition (Def 6.1, 6.2)
    • Thm 7.5 Soundness
    • Thm 7.6 Parallel Liveness
    • Thm 7.7 Post-Quantum Safety
  • ~/work/lux/proofs/quasar-cert-soundness.tex:
    • App B — ML-DSA-65 R1CS constraint count (~2^22.5 per verification; per-cert amortized to ~2^20 via shared-matrix optimization for n=21 validators)
    • App C — Static vs adaptive corruption (Fischlin / erasure hybrids)
    • App D — Trusted-setup ceremony (Bowe-Gabizon-Miers), PLONK upgrade path
    • App E — Pulsar parameter tightness: classical 2^142, quantum 2^130 via BDGL sieving + Grover speedup

Domain separation

All ML-DSA/SLH-DSA callers bind signatures to a context string per FIPS 204/205:

Context Used by File
lux-x-chain-utxo-v1 UTXO Fx plugins utxo/mldsafx, utxo/slhdsafx
lux-evm-precompile-mldsa-v1 EVM precompile precompile/mldsa/contract.go
lux-evm-precompile-slhdsa-v1 EVM precompile precompile/slhdsa/contract.go
lux-reshare-v1 Key resharing HKDF threshold/mpc
lux-wave-v1 Wave voting consensus/protocol/wave

No collisions. See crypto/mldsa.SignCtx/VerifySignatureCtx (same for slhdsa).

Transport

Inter-node: ZAP (github.com/luxfi/zap), NOT p2p or gRPC/protobuf.

Package Layout

consensus.go          Root facade, type aliases, NewChain/NewDAG/NewPQ
config/               Parameter presets (single, local, testnet, mainnet)
core/                 Core interfaces, dag structures
  dag/                DAG store, event horizon, ordering
engine/               Consensus engines (Chain, DAG, PQ)
  chain/              Linear chain engine
    bootstrap/        Fetch+execute initial sync to the beacon-named frontier
    summary/          State-summary adoption — runs BEFORE bootstrap
    syncer/           Consensus-pointer reconcile after an RLP import (unrelated)
  dag/                DAG engine
  pq/                 Post-quantum engine
  interfaces/         State enum (Unknown..Stopped)
protocol/             All Quasar sub-protocols (see table above)
types/                Block, Vote, Config, Decision, bag/
runtime/              VM wiring (chain IDs, validators)
pkg/wire/             Wire credentials (ML-DSA-44/65/87, BLS, Ed25519)
bench/                Benchmarks (ZAP throughput, Lux vs prior-gen EVM)
version/              Re-exports github.com/luxfi/version

Performance (Measured)

All numbers below are measured on Apple M1 Max (10 cores, darwin/arm64), CPU path only unless noted. See BENCHMARKS.md for full raw output and reproduce commands.

ZAP Wire Protocol (bench/)

Raw wire message throughput (ZAP transport microbench), NOT consensus finality TPS.

Config Wire messages/sec
Single connection 114K msg/s
20 parallel connections 376K msg/s
50 conns + batch 1000 20.26M msg/s

Protocol Microbenchmarks

Component Operation Time/Op
Wave Vote round 3.38 us
Photon K-of-N selection 3.03 us
Luminance Brightness update 72 ns
Quasar QuantumHash 435 ns
Quasar Validator add 328 us

QuasarCert Components (measured CPU path)

Per-component CPU costs for QuasarCert production and verification:

Operation Time Source
BLS sign (single) 350 us crypto/bls BenchmarkSign
BLS verify (single) 820 us crypto/bls BenchmarkVerify
BLS aggregate 100 sigs 5.3 ms protocol/quasar BenchmarkBLSAggregation/100
BLS aggregated verify (100 signers) 875 us protocol/quasar BenchmarkBLSAggregatedVerification/100
ML-DSA-65 sign 495 us crypto/mldsa BenchmarkMLDSA_Sign
ML-DSA-65 verify 181 us crypto/mldsa BenchmarkMLDSA_Verify
ML-DSA-65 verify (via Fx) 254 us utxo/mldsafx BenchmarkMLDSA65Verify
ML-DSA-65 verify (cached) 3 us utxo/mldsafx BenchmarkMLDSA65VerifyCached
SLH-DSA-192f verify 1.92 ms utxo/slhdsafx BenchmarkSLH192fVerify
Quasar full block (BLS+ML-DSA+Pulsar) 1.85 ms protocol/quasar BenchmarkQuasarBlockProcessing

QuasarCert verify (approx CPU, single cert, n=21 validators):

  • BLS aggregate verify: ~875 us (constant in signer count)
  • Groth16 proof verify: ~1-3 ms (pairing-dominated, not yet in our bench harness — see App B of proof sketch)
  • Pulsar threshold verify: variable, amortized O(1) after DKG
  • Total: ~2-5 ms per cert, GPU batch can amortize 10-100x across certs

Note on the stale 357 us claim in older papers: The "357 us epoch finality" from earlier Lux drafts (lux-triple-proof-consensus, lux-master-security-model, lux-performance-security-tradeoffs) does not match any measured operation in the current code. Closest real candidates: BLS single keygen (350 us), ML-DSA-65 sign (495 us), Groth16 proof size is 192 B but prover time is ~400 ms CPU / ~5-15 ms GPU for the full ML-DSA-65 verification circuit (App B). Papers should quote the measured 2-5 ms CPU QuasarCert verify.

Signature Schemes Benchmark (crypto + utxo Fx)

Scheme Single Verify Cached Ratio vs secp256k1
secp256k1 (C, native) 658 ns — 1.0x
P-256 (Go stdlib) 121 us 1.0 us 184x
Ed25519 (Go stdlib) 205 us 1.1 us 312x
ML-DSA-44 140 us — 213x
ML-DSA-65 250 us 3.0 us 380x
ML-DSA-87 420 us — 638x
SLH-DSA-SHA2-192f 1.92 ms 131 us 2912x
BLS (single verify) 820 us — 1246x

CostPerSignature values in UTXO Fx plugins are benchmarked-proportional.

GPU Primitives (Metal, Apple M1 Max)

Operation Time Throughput
MatMul (dense) 399 us 20.0 GB/s
Add (elementwise) 336 us 238 MB/s
NTT (N=8, CPU fallback) 461 ns —
PolyMul (N=8, CPU fallback) 1.5 us —
FieldMul 2.2 us —

GPU batch verify kicks in at ≥64 signatures (accel.BLSBatchVerifyThreshold). Below that, the CPU single-verify path is faster due to kernel dispatch overhead. Raw Metal dispatch is ~100 us minimum; the break-even for ML-DSA is around 64 signatures.

EVM (evmgpu core, CPU only)

Operation Time
InsertChain empty block 171 us (5844 blocks/sec)
InsertChain value-tx block 246 us (4067 blocks/sec)

ring-call benchmarks (ring200, ring1000) currently hit a pre-existing nil-pointer in core/types.Header.Hash at bench_test.go:306 when running full chain-read; unrelated to consensus correctness but flagged for evmgpu repo.

Lux vs prior-gen EVM (bench/)

ZAP deserialization: 157x faster than protobuf (21 ns vs 3231 ns, zero allocs). Wire deserialization throughput: 11.5M msg/s (Lux ZAP) vs 246K (protobuf baseline) — a raw serialization microbench, NOT consensus finality TPS. Run: GOWORK=off go test -v -run TestLuxVsLegacy_EndToEnd -bench=. ./bench/

Key Technical Notes

The finality ladder's two floors (engine/chain + config)

Both accept rungs carry a STAKE floor and a distinct-SIGNER floor, and neither half is sufficient alone. QuorumCert.VerifyWeighted enforces both, recomputed from the authoritative validator set at the cert's epoch:

rung set signers stake authorizes
Nova n ≥ 1 ≥ chain.NovaSignerFloor(n) > config.HalfStakeFloor(signer) local execution (reorgable)
Quasar n ≥ minBFTCommittee (4) ≥ config.TwoThirdsCount(n) > config.TwoThirdsStakeFloor(signer) export — bridges, DEX, cross-chain

A certificate also DECLARES a quorum, in QuorumCert.Threshold, and that number must be exactly chain.SignerFloor(tier, n) — the floor the set derives for the rung. A certificate states its quorum; it does not choose it. Without the clause the field is load-bearing: Verify's last clause counts distinct valid accepts against the certificate's OWN threshold, so one declaring 1 clears it on a single signature, and only the floors above stand between that and finality. The rule is EQUALITY, not a lower bound — an over-claim names a bar this set does not set, and tolerating it would let a certificate redefine the rung upward exactly as tolerating an under-claim lets it redefine the rung down. SignerFloor is the ONE definition (Rust finality::signer_floor, C++ signer_floor(Tier, n)), read by the rung predicates, by the assemblers, and by this clause, so the number enforced and the number declared cannot drift.

TWO derived counts, because there are two predicates — and reading the wrong one makes a node refuse its own certificates. chain.SignerFloor is the distinct-signer GUARD the weighted road carries beside a stake clause; chain.Quorum is the count that IS the predicate where no stake clause stands beside it (the count-only road). They agree at Quasar — the export supermajority is TwoThirdsCount(n) either way — and DIVERGE at Nova past five signers, because NovaSignerFloor saturates at three: correct as a guard on a road whose majority is carried in stake, and wrong as a majority on a road that has only the count. So assembleCertLocked declares NovaSignerFloor(n) on the stake arm and NovaQuorum(n) on the nil-stake arm, and each road's derived clause must read the number its own assembler declares. Every committee the test fixtures use is 4 or 5, which is exactly where the two agree — TestTheTwoRoadsDeriveTheNumberTheyDeclare walks 1..64 so the divergence is stated rather than discovered at K=6 in production.

Four corollaries, all load-bearing:

  • Minting states no quorum, and mints nothing without a set. BuildVerifiedQuorumCert took the floor as an ARGUMENT — the caller's alpha — and on the count-only road nothing re-derived it: one vote and an alpha of one minted an authority token on a chain of five, while a certificate carrying those exact bytes was refused on arrival. So there is no argument. The floor is SignerFloor(tier, SignerCount(epoch)), read at the door, and a nil stake source is ErrCertNeedsStake at BOTH rungs: a floor is a property of the set, and with no set there is no floor and nothing to mint. This is what Rust's Tally::cert and QuasarConsensus::create_certificate and C++'s Cert::verify_weighted already had by their types — each takes a set by reference, so the absent case is unrepresentable rather than checked — and Go now agrees by rule where they agree by construction.
  • The arrival roads read NO floor of their own. HandleIncomingCert and VerifyCatchupCertificate each carried a cheap pre-filter over the sample committee — NovaQuorum(K) for Nova, Alpha() for Quasar — in front of verifyCert. That is a THIRD spelling of a quantity with one definition, and it is a different quantity: the gate derives the accept rung's floor from the SIGNING SET, where NovaSignerFloor saturates at three, while the door read the committee's majority, which grows with K. At six seats the gate accepts a certificate declaring three and the door demanded four; at eleven it demanded six; and where the set is smaller than the committee the door asked a set of three for the four-seat committee's three when the set derives two. An honest certificate — the exact bytes the node's own assembler produces — never reached the predicate that would have admitted it. A door in front of a gate may only refuse what the gate refuses, so the doors are gone and the floor is read once, where it is authoritative. Nothing is lost: the gate demands EQUALITY with the derived floor, which is strictly stronger than any "at least" a door applied, and no work is saved by refusing early — the dropped certificates were the ones carrying FEW votes, which are the cheap ones. TestTheDoorAdmitsWhatTheGateAdmits walks the sizes where the two numbers part.
  • verifyCert derives on the arrival road's own terms. It ADMITS a certificate that arrived carrying N real signatures from N distinct in-set validators — all that was missing is that its declared quorum was its own. With a stake source it is VerifyWeighted; without one the floor comes from effectiveCommittee, that road's authoritative view of its set. Not "an equal-stake chain cannot export", but "it may not export on a quorum the certificate wrote for itself". Without the clause a gossiped Quasar cert declaring a quorum of ONE, over one signature, reached AcceptWithCert. That committee view is weaker than a stake source (it is K, the sample size), and that asymmetry is why the MINTING door refuses without a set rather than weighing a configured number: a committee is strong enough to CHECK a declaration and not strong enough to ORIGINATE one. The price, and it is real: with no stake model the committee is configured, so this road now needs the fleet to agree on K — two nodes that disagree about their committee disagree about which certificates exist, where before any declaration was admitted and they interoperated by accident.
  • Every assembler reads the same n the verifier does. assembleCertLocked derives Nova's declared floor from stakeSource.SignerCount, not from effectiveCommittee (which sizes the SAMPLE and is floored at minBFTCommittee). At three signers the two disagreed — the sampler said three, the set said two — so a node would have built a certificate its own peers refuse. attestation.go's export trigger reads SignerFloor(Quasar, SignerCount), the same function, so the trigger and the rule cannot part.

SignerFloor and Quorum return int, not uint32: the floor is counted at the width the SET is counted at, and narrowed only where a certificate is written. Narrowing first wraps — ⌊2n/3⌋+1 passes 2³² at about six billion signers, and a wrapped floor of 1 is a floor a lone signer meets. Because the derived clause compares at full width, a mint that narrowed is caught by it rather than believed.

Five things produce the authority token, and every one of them runs the full predicate first or has no votes to run it on:

  1. BuildVerifiedQuorumCert — the exported minting door, from raw votes over a set.
  2. HandleIncomingCert — a gossiped certificate, through verifyCert.
  3. VerifyCatchupCertificate → AcceptCatchupBlock — the same, on the catch-up road.
  4. assembleVerifiedCertLocked → assembleCertLocked — the engine's own assembly, which verifies before it caches.
  5. buildSingleValidatorCertLocked — the K==1 road, and the one that is NOT a predicate: on a genuinely single-validator chain it synthesizes a zero-vote Nova token from the position, because the sole validator's own accept IS the quorum and there is no other party to protect against. It is fenced three ways — both callers gate on K()==1, it prefers a real signed 1-of-1 whenever one assembles, and it returns the ZERO token (which AcceptWithCert refuses) once the live count passes one on a chain that has finalized anything. It is Nova only, so it is never an export door.

Both numbers are read over the SIGNERS: signer is StakeSource.SignerStake and n is StakeSource.SignerCount, never the membership roll. A member the chain carries without a key is a spectator — it holds stake and can never cast a vote any verifier accepts — and counting it would raise a bar it cannot help clear. Past a third of what the chain carries, ⅔ becomes arithmetically unreachable and export is stranded with every signer agreeing; past a half, so is Nova. The corpus freezes both halves separately, because a set can be stranded in stake or in seats: quasar_keyless_stake (six signers, a seventh member holding a third of the roll — the count floor is 5 either way, so only the stake denominator decides) and quasar_keyless_count (four signers, two members holding one unit each — the stake clears under either reading, so only the count denominator decides).

This is the ⅔ rule read over the right set, not a weakening of it. Quorum intersection is untouched — two disjoint quorums each past ⅔ of the signer stake would sum past the whole of it — and the fault budget simply stops being spent on parties known in advance to cast no vote.

The export rung's third floor: the SET

Reading both floors over the signers lets a set SHRINK to its signers, and a rung whose floors are read over n is satisfiable at every n — TwoThirdsCount(1) is 1, so one signer over a one-signer set clears the count and the stake floors together. Byzantine tolerance is f = ⌊(n−1)/3⌋, which is ZERO for n of one, two and three: such a certificate absorbs no fault, and one compromised key among its signers forges export finality outright.

So the export rung refuses a signing set below minBFTCommittee — the same constant bftCommittee floors committee SELECTION at, read at the certificate instead of at the sampler. Nova has no such clause and must not grow one: it authorizes local execution the chain can still reorg away, and a small or partitioned chain has to be able to make progress. A chain with three validators therefore PRODUCES and never CERTIFIES; operators need n ≥ 4 to export with any fault tolerance at all. The corpus freezes this as quasar_keyless_third, whose three unanimous signers clear both quorum floors and are refused anyway.

The C++ engine enforces the same shape from the other direction: Validator is a public key there and the set is keyed by one, so a spectator has no slot to occupy — and since a PubKey is a fixed array, presence proves nothing, so its constructor now runs bls::key_validate on every seat. A key that is not a canonical, in-subgroup, non-identity G1 point is refused at construction rather than admitted and silently unable to sign. "A validator is a signer" is enforced there, not assumed.

Carried stake vs signer stake, at the seam

StakeSource reports both. SignerStake/SignerCount are the denominators every floor is read against; CarriedStake is the membership roll's weight and NO floor may read it. It exists so the gap is visible: a chain whose signable stake has fallen to a sliver of what it carries keeps certifying correctly at ⅔ of the sliver, and ResponsiveStakePct — which is voted/signer — stays healthy the whole way down. FinalityStatus.SignerStakePct (signer/carried) is the number that falls, and the one to alarm on. Rust's ValidatorSet::carried, C++'s QuorumCertEngine::total_stake.

The reason for a count floor at all: a threshold read only in stake reports one party wherever the stake is concentrated in one validator. Two thirds of the stake is one signature there, and a certificate one key can mint is not a claim that a Byzantine supermajority agreed. Nova's floor saturates at three (it asks only "more than one party?" before a REORGABLE local execution); Quasar's is the supermajority itself, ⌊2n/3⌋+1, and grows with the set.

config.TwoThirdsCount is the ONE definition of that count and it is derived from config.TwoThirdsStakeFloor over unit weights, so the count and the stake predicate cannot drift. It has two uses and they are the same rule from two sides: FeasibleParams sizes α to it, and the verifier enforces it. It is not configurable — a signer floor an operator can set is a security parameter chosen by the party it constrains. Rust two_thirds_count, C++ two_thirds_count, same number.

Quasar reads STAKE FIRST and the count second. Both must hold, so the order decides only which clause a refusal is NAMED by: on an equal-stake set the two are one bar in two units and bind together, and the count clause can bind ALONE only where the weights are lopsided — so reaching it means the stake was there and the signers were not. Nova orders the other way because its count floor is a lower, saturating, stake-independent bar, a different question asked first.

Liveness, held by engine/chain/quasar_count_floor_test.go: QuasarAttestor (the only producer of export certs) assembles at exactly TwoThirdsCount(n) over the live set, which is the number the verifier recomputes — so the clause refuses nothing the engine can produce. The gossip door Runtime.HandleIncomingCert applies is FeasibleParams' α, and what is proven of it is Alpha == TwoThirdsCount(p.K): the same function, read at the chain's K rather than at the live n. K and n are one number whenever the params were sized to the set that is running. Where they part the verifier is the authority — a door under it admits what the predicate then refuses, and a door over it is the gossip-admission bound the runtime already had, not something this clause added.

Test Status

  • All tests pass except TestQuantumBundle_ChainIntegrity which is flaky (Pulsar threshold signing nondeterminism -- passes on retry)
  • TestNovaQuasarMatrix_FourOfFive_ExportsQuasar is also FLAKY (pre-existing, observed at HEAD 35ab4b25 with the change stashed: 3 fails / 1 pass across isolated and full-suite runs, CGO_ENABLED=0; the fail is the 25s export liveness waitFor expiring with tips=map[]). Not introduced by any test-only change — needs a look from whoever owns the steer-site work.
  • Build: GOWORK=off go build ./...
  • Tests: GOWORK=off go test -count=1 -short -timeout 300s ./...
  • Coverage: go test -covermode=count -coverprofile=c.out ./engine/chain/... ./validator/... ./config/... ./conformance/... then go tool cover -func=c.out. make coverage-html renders it.

The clauses coverage cannot reach, and why

Four statements in the consensus core are unreachable through any public entry point. They are walls behind walls, they are correct, and chasing them costs an afternoon each — so they are written down instead.

  • cert.go verifyNovaMajority/verifyQuasarSupermajority's COUNT clauses (VoterCount() < SignerFloor(...)) — unreachable under an IDEMPOTENT source, and not otherwise. Verify proves count >= Threshold and the derived clause proves Threshold == SignerFloor(n), so count >= SignerFloor(n) follows — but each clause reads SignerCount for itself, and a source that answers the two reads differently makes them two numbers. Red raced 3,600 pairs against a source that moves between reads; in 1,924 of them these count clauses were the ONLY clause that refused, and they refused fail-closed. So they are not dead code kept for a reader: they are the guard that survives a set moving under the predicate, and they are where the floor is DEFINED. Rust and C++ carry the same redundancy for the same reason. The ISSUE road has no such race — Tally::cert reads the count once and answers every clause of one issue with it — because there the two numbers would be the assembler disagreeing with itself. This is also why the corpus's under-quorum rows are now named belowThreshold by Verify rather than by the rung.
  • cert.go VerifyWeighted's default: unknown-tier arm. Verify runs first and refuses any tier outside {Nova, Quasar}, so the switch below it never sees one. TestATierIsRefusedByVerifyBeforeVerifyWeightedSeesIt pins that ORDER, so a reordering that made the arm reachable fails a test rather than passing quietly. Rust carries the identical dead arm at cert.rs:416.
  • pop.go Verify's canonical re-compress check, and Verify's own Message error. bls.PublicKeyFromCompressedBytes is canonical — probed over all 256 spellings of byte 0 and every single-bit flip of a real key, zero decode to a point that re-compresses differently — a property of the RE-CAPTURE, where 9 of the 13 probe inputs were rewritten, and not of a run that happened to find none — and the width was checked two lines above. Rust's pop.rs:83 and C++'s bls.cpp:145 are the same wall.
  • registration.go Register's decode after pop.Verify succeeded, which decoded the same bytes. C++ marks its twin unreachable: pop_verify decoded it.
  • attestation.go Ingest's AssembleQuorumCert error: every input is already known good there (Quasar tier, threshold ≥ 1, accept-only, map-distinct voters, count ≥ threshold).

Reachable only with a filesystem fault seam, which does not exist: the four I/O failure legs inside writeFileAtomic (write, sync, close, rename) and fsyncDir's Sync error. The rest of both paths — a temp file that outlives a write, a destination whose directory is gone, a directory that cannot be opened — is covered.

Not a coverage question but found by asking one: ErrQCVotePosition is declared in cert.go and returned by nothing. The var block above it says each error maps 1:1 onto a predicate clause; that one maps onto no clause, because a vote that signed a different position fails as a bad SIGNATURE — the message is rebuilt from the cert's own position and never from the vote's, so there is nothing left to compare. A reader building a cross-implementation error corpus would take the comment at its word and look for the clause.

Restart preserves state — a REAL assertion now (engine/chain)

TestRestartPreservesState used to check only IsBootstrapped/HealthCheck flags across Stop/Start — an engine that dropped every accepted block on Stop still passed. It now drives a real signed 4-of-5 quorum accept (Nova) through the Quasar export cert BEFORE the stop, asserts the consensus-critical triple (accepted set via finalizedByCert, Preference(), QuasarHeight()) survives the restart unchanged, and then finalizes + certifies a SECOND height on the restarted engine. Negative control executed: a Stop that clears finalizedByCert fails the test at "restart must not lose an accepted block" — the old body passed under that same mutilation.

State-summary adoption (engine/chain/summary)

The client half of state sync: it decides which summary a node adopts before it bootstraps, so a node whose trie is damaged or whose gap exceeds the descent window has a way back that is not a reseed. New(Config).Run(ctx) returns (Outcome, error) — OutcomeAdopted (wait for the VM's done signal, then bootstrap the tail from the summary height), OutcomeSkipped (bootstrap from the local tip; nothing was destroyed), and a zero OutcomeInvalid that can never read as adopted.

Two rounds. DISCOVERY (Source.Offers) asks the connected beacons what each holds; its only product is a list of candidate heights and it carries no threshold — a wrong height costs one entry in the next request. RATIFICATION (Source.Ballots) asks the WHOLE beacon set, never a sample, and adopts only when a summary's stake strictly exceeds ⅔ of the stake that answered AND the stake that answered exceeds half the whole set (config.TwoThirdsStakeFloor / config.HalfStakeFloor — the same definitions the cert verifier uses). The second term is what stops an eclipse from shrinking the denominator until a sliver of stake looks like a supermajority.

Load-bearing details, each pinned by a test that a mutation kills:

  • A candidate at or below the local tip is dropped before the vote, not left for the VM to refuse inside the state-destroying Accept.
  • One beacon, one voice: ballots dedup by NodeID, and ids dedup within a ballot.
  • Selection ranges over the candidates, so an id nobody offered cannot win however much stake names it.
  • Stake overflow refuses the round; it never saturates.
  • An interrupted sync (GetOngoingSyncStateSummary) enters as an ordinary candidate — that is what lets it be ratified when no beacon offers it — and wins only among the summaries that clear the bar.
  • Static and dynamic modes both wait for the VM. Bootstrap re-executes from the summary height against the state below it, so "fetch in the background" fails those executions for reasons unrelated to the network.

Zero transport lives here: no request ids, no pending/failed sets, no timeout manager, no validator set. Source is two blocking, window-bounded calls the node implements, exactly as bootstrap.BlockSource is. Not to be confused with engine/chain/syncer/, which reconciles consensus pointers after an RLP import.

SDK Status (Honest Assessment)

  • Go: Production-ready (protocol/, engine/, core/)
  • Python (pkg/python/): Only complete non-Go SDK with real consensus logic
  • C (pkg/c/): Data structures only, not real consensus
  • Rust (pkg/rust/): pure Rust, no FFI. Conformant on the finality standard, partial on the protocols — see below.
  • C++ (pkg/cpp/): Stub

Rust binding — what it is held to

conformance/corpus.json is the standard as data, generated from the Go definitions by conformance/corpus.go. pkg/rust/tests/conformance.rs reads that file and holds the crate to it. Regenerate with go test ./conformance -update; the Rust tests fail if the two drift.

Conformant and checked against the corpus: the signed vote message (226 bytes under LUX/chain/vote/v2\0), the certificate wire form byte for byte, both stake floors across the full u64 range, the count thresholds, the ladder, the per-epoch FPC seed, and θ/α to the exact IEEE-754 bit over 192 recorded cases.

NOT conformant, and known:

  • The sampler's NodeID is 32 bytes; luxfi/ids.NodeID and every certificate on the wire use 20. finality::Node is the correct width; quasar has not been moved onto it, so a node's sampling identity and its signing identity are populated separately by the caller.
  • wave accumulates one cumulative tally per block. Go samples K fresh votes per round. The phase now advances per counted round rather than per vote, which is Go's placement, but the round model itself still differs.
  • QuasarConfig::alpha is an f64. config/constants.go requires an integer quorum predicate and says so.
  • There is no P2P, no block store and no engine loop. The crate computes; it does not participate.

bls carries the ciphersuite Lux signs under — BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_, public keys 48 bytes in G1, signatures 96 bytes in G2, matching luxfi/crypto/bls.

benches/consensus_bench.rs measures the round and certificate hot paths. Signature verification is per-vote, as Go's QuorumCert.Verify is, and it dominates: ~0.8 ms a signature, so a 21-signature certificate is ~14.5 ms (Ryzen, --measurement-time 1.5). Aggregate verification would collapse that to one pairing, but only behind proof-of-possession — without it, aggregation is a rogue-key attack.

Dependencies (Critical)

  • github.com/luxfi/crypto -- BLS, ML-DSA, threshold signing
  • github.com/luxfi/corona -- Ring-LWE threshold signatures
  • github.com/luxfi/zap -- Zero-copy wire protocol
  • github.com/luxfi/ids -- ID types
  • github.com/luxfi/version -- Version management

ValidatorSchemeID — cross-axis NodeIDScheme gate

config/validator_scheme.go (v1.23.4) owns the rule that pins which NodeIDScheme byte a peer / proposer / validator / mempool sender MUST present on this chain.

  • ChainSecurityProfile.ValidatorSchemeID() reads through IdentitySchemeID (one byte, one source of truth). Returns SigSchemeMLDSA65 (0x42) on the canonical strict-PQ profile.
  • ChainSecurityProfile.AcceptsValidatorScheme(presented, classicalCompatUnsafe) is the cross-axis check the node-side peer.SchemeGate funnels every inbound NodeID through. Strict-PQ profiles refuse classical (0x90) regardless of the operator flag (defence-in-depth); permissive profiles honour the flag.
  • ErrValidatorSchemeMismatch is the typed error consumers match against; mirrors ids.ErrNodeIDSchemeMismatch at the wire-decode layer.

The wire bytes are shared with luxfi/ids.NodeIDScheme. A test pins the alignment in literal form so a renumber in either enum trips CI.

Bag Package

Canonical location: types/bag. All repos should import from here.

Version

Managed via github.com/luxfi/version (re-exported in version/). Do not hardcode version strings in this repo.

Rules

  1. ALWAYS use GOWORK=off for go commands in this repo
  2. NEVER bump packages above v1.x.x
  3. NEVER use go-ethereum or ava-labs packages -- use luxfi
  4. Update THIS file (LLM.md) with significant discoveries
  5. CLAUDE.md and AGENTS.md are symlinks to LLM.md -- do not commit them
  6. Show tests passing, do not just claim "done"