A certificate carries a Threshold, and until now that field was its own authority on the count-only road: Verify's last clause counts distinct valid accepts against the number the CERTIFICATE names, so one declaring 1 clears it on a single signature. C++ has always demanded the derived floor exactly, so the same certificate was accepted by two implementations and refused by the third. VerifyWeighted now requires Threshold == SignerFloor(tier, n) — the floor this set gives this rung. Equality and 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 of a rung's count floor, read by the rung predicates, by the assemblers and by the new clause, so the number enforced and the number declared cannot drift. Two corollaries the rule forces: No set, no export. BuildVerifiedQuorumCert refuses any tier that authorizes export when no stake source is supplied. That road counts votes against the caller's own alpha, so an export token could be minted over one signer with alpha of one — and AcceptWithCert trusts the token, which is the point of the token. The accept rung keeps the road: it authorizes local execution the chain can still reorg away. The assembler reads the same n the verifier does. assembleCertLocked derived Nova's declared floor from effectiveCommittee, which sizes the SAMPLE and is floored at the minimum Byzantine committee. At three signers the sampler said three and the set said two, so a node would have built a certificate its own peers refuse. The export path already read SignerCount. The corpus grows five rows — three that declare a quorum of their own, and two lopsided sets where the stake half binds alone, which an equal set cannot state because both halves bind at one edge there. Two frozen verdicts change the clause they are NAMED by, from the stake floor to the count, and neither changes from refused: over an equal set a certificate short of its stake is also short of the quorum it now has to declare. The rungs' own count clauses are unreachable as a result — Verify proves count reaches the declaration and the new clause proves the declaration is the floor — and are kept as the place the floor is defined and as the guard that survives any change to the declaration rule. Written down in LLM.md beside the other walls behind walls. Co-authored-by: Hanzo Dev <[email protected]>
37 KiB
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 inprotocol/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 corollaries, both load-bearing:
- No set, no export.
BuildVerifiedQuorumCertrefuses any tier for whichFinality.AuthorizesExportholds whenstakeis nil (ErrExportNeedsStake). The count-only road counts votes against the caller's own alpha, so an export token could be minted over one signer withalpha=1. The accept rung keeps that road — it authorizes only local execution the chain can reorg away. - The assembler reads the same n the verifier does.
assembleCertLockedderives Nova's declared floor fromstakeSource.SignerCount, not fromeffectiveCommittee(which sizes the SAMPLE and is floored atminBFTCommittee). 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.goalready readSignerCountfor the export rung.
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_ChainIntegritywhich is flaky (Pulsar threshold signing nondeterminism -- passes on retry) TestNovaQuasarMatrix_FourOfFive_ExportsQuasaris also FLAKY (pre-existing, observed at HEAD35ab4b25with 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 withtips=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/...thengo tool cover -func=c.out.make coverage-htmlrenders 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.goverifyNovaMajority/verifyQuasarSupermajority's COUNT clauses (VoterCount() < SignerFloor(...)), since the derived-threshold rule landed.Verifyprovescount >= Thresholdand the derived clause provesThreshold == SignerFloor, socount >= SignerFloorfollows and neither clause can fire. They are kept deliberately: they are where the floor is DEFINED for a reader, and they are the guard that survives any future change to the declaration rule — removing them would leave the count floor resting entirely on a clause about a wire field. Rust and C++ carry the same redundancy for the same reason. This is why the corpus's under-quorum rows are now namedbelowThresholdbyVerifyrather than by the rung.cert.goVerifyWeighted'sdefault:unknown-tier arm.Verifyruns first and refuses any tier outside {Nova, Quasar}, so the switch below it never sees one.TestATierIsRefusedByVerifyBeforeVerifyWeightedSeesItpins that ORDER, so a reordering that made the arm reachable fails a test rather than passing quietly. Rust carries the identical dead arm atcert.rs:416.pop.goVerify's canonical re-compress check, andVerify's ownMessageerror.bls.PublicKeyFromCompressedBytesis 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 — and the width was checked two lines above. Rust'spop.rs:83and C++'sbls.cpp:145are the same wall.registration.goRegister's decode afterpop.Verifysucceeded, which decoded the same bytes. C++ marks its twinunreachable: pop_verify decoded it.attestation.goIngest'sAssembleQuorumCerterror: 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
NodeIDis 32 bytes;luxfi/ids.NodeIDand every certificate on the wire use 20.finality::Nodeis the correct width;quasarhas not been moved onto it, so a node's sampling identity and its signing identity are populated separately by the caller. waveaccumulates 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::alphais anf64.config/constants.gorequires 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 signinggithub.com/luxfi/corona-- Ring-LWE threshold signaturesgithub.com/luxfi/zap-- Zero-copy wire protocolgithub.com/luxfi/ids-- ID typesgithub.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 throughIdentitySchemeID(one byte, one source of truth). ReturnsSigSchemeMLDSA65(0x42) on the canonical strict-PQ profile.ChainSecurityProfile.AcceptsValidatorScheme(presented, classicalCompatUnsafe)is the cross-axis check the node-sidepeer.SchemeGatefunnels every inbound NodeID through. Strict-PQ profiles refuse classical (0x90) regardless of the operator flag (defence-in-depth); permissive profiles honour the flag.ErrValidatorSchemeMismatchis the typed error consumers match against; mirrorsids.ErrNodeIDSchemeMismatchat 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
- ALWAYS use
GOWORK=offfor go commands in this repo - NEVER bump packages above v1.x.x
- NEVER use go-ethereum or ava-labs packages -- use luxfi
- Update THIS file (LLM.md) with significant discoveries
- CLAUDE.md and AGENTS.md are symlinks to LLM.md -- do not commit them
- Show tests passing, do not just claim "done"