# 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 `EvidenceKind`s** (`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 `EvidenceKind`s **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: ```go 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"