mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
The Quasar cert layers are the right cardinality of "Double Lattice +
Identity Rollup + optional Classical Fast-path", not "Triple Quantum":
BLS-12-381 classical fast-path (pairing) — empty in pure-PQ
Corona Ring-LWE threshold ML-DSA (luxfi/corona; production R-LWE)
Pulsar Module-LWE threshold ML-DSA (luxfi/pulsar; Class N1 byte-equal
to FIPS 204)
MLDSARollup per-validator ML-DSA-65 rolled into one STARK / Groth16 proof
(Z-Chain → P3Q)
"Triple Quantum" misled because BLS is classical — at most one of three
legs was quantum in the historic BLS+Corona+ZK shape. The honest PQ
cardinality is **Double Lattice** (Ring + Module). Pure-PQ profiles
drop BLS entirely and rely on Corona + Pulsar.
Corona is the academic R-LWE construction with trusted-dealer DKG;
it stays out of any open public-chain path. Corona is the production
Ring-LWE library (Pedersen DKG over R_q + proactive resharing).
Surface (in protocol/quasar/types.go):
QuasarCert.BLS → unchanged (classical fast-path, optional)
QuasarCert.Corona → QuasarCert.Corona (Ring-LWE threshold sig)
QuasarCert.MLDSAProof → QuasarCert.MLDSARollup (succinct identity rollup)
QuasarCert now also has a Pulsar field (Module-LWE threshold sig,
populated when wired)
QuasarSig.Corona → QuasarSig.Corona (also added Pulsar []byte)
FinalityEvent.Corona / MLDSAProof → Corona / Pulsar / MLDSARollup
New predicates:
QuasarCert.IsDoubleLattice() — Corona ∧ Pulsar (honest PQ count)
QuasarCert.HasClassicalFastPath() — len(BLS) > 0
QuasarCert.HasIdentityRollup() — len(MLDSARollup) > 0
The wire byte layout is unchanged (length-prefixed slots). Callers that
read the bytes don't notice. Source-level references that compiled
before the rename now compile against the new identifiers; bulk
rename swept the consensus repo (22 files, 219 insertions, 151
deletions) plus the engine/pq facing FinalityEvent.
Tests: GOWORK=off go test -short -count=1 ./... → all 28 packages green
(quasar suite 32s; integration 60s; no regressions).
478 lines
14 KiB
Go
478 lines
14 KiB
Go
// Copyright (C) 2025, Lux Industries Inc All rights reserved.
|
|
// Quasar implementation - the supermassive black hole at the center of the blockchain galaxy.
|
|
|
|
package quasar
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/config"
|
|
)
|
|
|
|
// quasarEngine implements the Engine interface.
|
|
type quasarEngine struct {
|
|
mu sync.RWMutex
|
|
cfg Config
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
// Block processing
|
|
incoming chan *Block
|
|
finalized chan *Block
|
|
|
|
// State
|
|
finalizedBlocks map[string]*Block // hash -> block
|
|
height uint64
|
|
startTime time.Time
|
|
|
|
// Consensus engine
|
|
certifier *Certifier
|
|
signer *signer // real BLS+Corona+ML-DSA signer (optional, may be nil for legacy)
|
|
|
|
// Metrics
|
|
processed uint64
|
|
}
|
|
|
|
var (
|
|
// ErrThresholdTooLow is returned when the consensus threshold is below the
|
|
// minimum safe value of 2. Single-node mode must use NewTestEngine.
|
|
ErrThresholdTooLow = errors.New("QThreshold must be >= 2 for production consensus")
|
|
|
|
// ErrPartialTripleCert is returned by generateCert when the chain's
|
|
// ChainSecurityProfile demands a triple-mode (P+Q+Z) certificate but
|
|
// the signer can't produce all three layers — typically because no
|
|
// validator is configured with Corona shares (the Q layer). Closes
|
|
// CR-10: previously this path fell through to a SHA-256 placeholder
|
|
// and the engine would emit a cert the network believes is triple
|
|
// when it's only single-layer.
|
|
ErrPartialTripleCert = errors.New("Certifier: refusing to emit partial cert under triple-mode profile")
|
|
)
|
|
|
|
// NewEngine creates a new Quasar consensus engine.
|
|
// Threshold must be >= 2 for production use. For single-node testing, use NewTestEngine.
|
|
func NewEngine(cfg Config) (Engine, error) {
|
|
if cfg.QThreshold < 2 {
|
|
return nil, ErrThresholdTooLow
|
|
}
|
|
threshold := cfg.QThreshold
|
|
|
|
certifier, err := newCertifier(threshold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create certifier: %w", err)
|
|
}
|
|
|
|
bufSize := 1000
|
|
return &quasarEngine{
|
|
cfg: cfg,
|
|
incoming: make(chan *Block, bufSize),
|
|
finalized: make(chan *Block, bufSize),
|
|
finalizedBlocks: make(map[string]*Block),
|
|
certifier: certifier,
|
|
}, nil
|
|
}
|
|
|
|
// NewTestEngine creates a Quasar engine with threshold=1 for single-node testing.
|
|
// Must NOT be used in production.
|
|
func NewTestEngine(cfg Config) (Engine, error) {
|
|
threshold := cfg.QThreshold
|
|
if threshold < 1 {
|
|
threshold = 1
|
|
}
|
|
|
|
certifier, err := newCertifier(threshold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create certifier: %w", err)
|
|
}
|
|
|
|
bufSize := 1000
|
|
return &quasarEngine{
|
|
cfg: cfg,
|
|
incoming: make(chan *Block, bufSize),
|
|
finalized: make(chan *Block, bufSize),
|
|
finalizedBlocks: make(map[string]*Block),
|
|
certifier: certifier,
|
|
}, nil
|
|
}
|
|
|
|
// Start begins the consensus engine.
|
|
func (q *quasarEngine) Start(ctx context.Context) error {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
q.ctx, q.cancel = context.WithCancel(ctx)
|
|
q.startTime = time.Now()
|
|
|
|
go q.processLoop()
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully shuts down the consensus engine.
|
|
func (q *quasarEngine) Stop() error {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
if q.cancel != nil {
|
|
q.cancel()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Submit adds a block to the consensus pipeline.
|
|
func (q *quasarEngine) Submit(block *Block) error {
|
|
if block == nil {
|
|
return fmt.Errorf("nil block")
|
|
}
|
|
|
|
select {
|
|
case q.incoming <- block:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("buffer full")
|
|
}
|
|
}
|
|
|
|
// Finalized returns a channel of finalized blocks.
|
|
func (q *quasarEngine) Finalized() <-chan *Block {
|
|
return q.finalized
|
|
}
|
|
|
|
// IsFinalized checks if a block is finalized.
|
|
func (q *quasarEngine) IsFinalized(blockID [32]byte) bool {
|
|
q.mu.RLock()
|
|
defer q.mu.RUnlock()
|
|
|
|
hash := hex.EncodeToString(blockID[:])
|
|
_, ok := q.finalizedBlocks[hash]
|
|
return ok
|
|
}
|
|
|
|
// Stats returns consensus metrics.
|
|
func (q *quasarEngine) Stats() Stats {
|
|
q.mu.RLock()
|
|
defer q.mu.RUnlock()
|
|
|
|
return Stats{
|
|
Height: q.height,
|
|
ProcessedBlocks: q.processed,
|
|
FinalizedBlocks: uint64(len(q.finalizedBlocks)),
|
|
PendingBlocks: len(q.incoming),
|
|
Validators: q.certifier.validatorCount(),
|
|
Uptime: time.Since(q.startTime),
|
|
}
|
|
}
|
|
|
|
// processLoop is the main consensus loop.
|
|
func (q *quasarEngine) processLoop() {
|
|
for {
|
|
select {
|
|
case <-q.ctx.Done():
|
|
return
|
|
case block := <-q.incoming:
|
|
q.processBlock(block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// processBlock processes a single block through consensus.
|
|
func (q *quasarEngine) processBlock(block *Block) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
q.processed++
|
|
|
|
// Generate quantum certificate
|
|
cert := q.certifier.generateCert(block)
|
|
if cert == nil {
|
|
return // Did not achieve consensus
|
|
}
|
|
|
|
// Vote-acceptance gate (CR-10): under a triple-mode profile the
|
|
// cert MUST carry every layer (BLS + Corona + MLDSAProof).
|
|
// QuasarCert.Verify enforces that structural property; rejecting
|
|
// here keeps the cert-acceptance gate honest even if a future
|
|
// generateCert path forgets to refuse silently. Belt and braces.
|
|
q.certifier.mu.RLock()
|
|
demandsTriple := q.certifier.demandsTriple()
|
|
q.certifier.mu.RUnlock()
|
|
if demandsTriple && !cert.Verify(nil) {
|
|
return
|
|
}
|
|
|
|
// Finalize block
|
|
block.Cert = cert
|
|
block.Hash = computeHash(block)
|
|
|
|
q.finalizedBlocks[block.Hash] = block
|
|
q.height++
|
|
|
|
// Notify listeners
|
|
select {
|
|
case q.finalized <- block:
|
|
default:
|
|
// Drop if buffer full
|
|
}
|
|
}
|
|
|
|
// SetProfile binds a ChainSecurityProfile to this engine. Forwards to the
|
|
// certifier so generateCert can enforce the triple-mode gate (CR-10).
|
|
//
|
|
// Strict-PQ / FIPS profiles refuse any cert below P+Q+Z. Non-strict
|
|
// profiles preserve legacy behaviour (placeholder + fallback OK).
|
|
func (q *quasarEngine) SetProfile(profile *config.ChainSecurityProfile) {
|
|
q.certifier.SetProfile(profile)
|
|
}
|
|
|
|
// computeHash computes a block hash.
|
|
func computeHash(block *Block) string {
|
|
h := sha256.New()
|
|
h.Write(block.ID[:])
|
|
h.Write(block.ChainID[:])
|
|
h.Write([]byte(block.ChainName))
|
|
h.Write([]byte(fmt.Sprintf("%d:%d", block.Height, block.Timestamp.Unix())))
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// Certifier handles certificate generation for the engine.
|
|
//
|
|
// When a Signer is attached via AttachSigner the certifier produces real
|
|
// QuasarCerts (BLS share + ML-DSA sig + Corona Round 1 commitment). With
|
|
// no signer, it falls back to deterministic SHA-256 commitments suitable
|
|
// only for in-process unit tests.
|
|
//
|
|
// Triple-mode enforcement (CR-10): when SetProfile attaches a strict-PQ
|
|
// or FIPS profile, generateCert REFUSES to fall back to the SHA-256
|
|
// placeholder OR to a non-triple realCert. The audit gate is profile-
|
|
// driven, not heuristic.
|
|
type Certifier struct {
|
|
mu sync.RWMutex
|
|
threshold int
|
|
validators map[string]int // validator -> weight
|
|
|
|
// Real PQ signing engine. Optional: when nil, the certifier uses the
|
|
// legacy SHA-256 fallback for backward compatibility with single-node
|
|
// engine tests.
|
|
signer *signer
|
|
signerCtx context.Context
|
|
|
|
// profile, when non-nil and strict-PQ / FIPS, demands triple-mode
|
|
// certificates. generateCert returns nil rather than emitting a
|
|
// SHA-256 placeholder or single-layer cert under such a profile —
|
|
// the engine's caller MUST then treat the round as unfinalised.
|
|
profile *config.ChainSecurityProfile
|
|
}
|
|
|
|
func newCertifier(threshold int) (*Certifier, error) {
|
|
return &Certifier{
|
|
threshold: threshold,
|
|
validators: make(map[string]int),
|
|
}, nil
|
|
}
|
|
|
|
// AttachSigner wires a real BLS+Corona+ML-DSA signer into the certifier.
|
|
// After this call, generateCert produces real cryptographic certificates.
|
|
func (h *Certifier) AttachSigner(ctx context.Context, s *signer) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.signer = s
|
|
h.signerCtx = ctx
|
|
}
|
|
|
|
// SetProfile binds a ChainSecurityProfile to this certifier. When the
|
|
// profile is strict-PQ or FIPS, generateCert refuses every code path
|
|
// that would produce a non-triple cert (the SHA-256 placeholder and the
|
|
// single-layer realCert fallback). Closes CR-10.
|
|
//
|
|
// Nil profile preserves legacy behaviour (placeholder + fallback OK).
|
|
func (h *Certifier) SetProfile(profile *config.ChainSecurityProfile) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.profile = profile
|
|
}
|
|
|
|
// demandsTriple reports whether the certifier's profile requires every
|
|
// cert to be triple-mode (P+Q+Z). Strict-PQ and FIPS demand it; other
|
|
// profiles do not. Caller MUST hold h.mu.RLock.
|
|
func (h *Certifier) demandsTriple() bool {
|
|
return h.profile != nil && h.profile.IsPQ()
|
|
}
|
|
|
|
func (h *Certifier) validatorCount() int {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return len(h.validators)
|
|
}
|
|
|
|
// generateCert creates a certificate for the given block. When a signer is
|
|
// attached it returns a real QuasarCert (BLS share + ML-DSA sig + Corona
|
|
// Round 1 commitment). Otherwise it falls back to SHA-256 placeholders for
|
|
// in-process tests.
|
|
//
|
|
// Under a strict-PQ / FIPS profile (CR-10) every fallback path is closed:
|
|
// generateCert returns nil unless a real triple-layer cert can be produced.
|
|
// The engine treats nil as "round did not finalise" and refuses to advance.
|
|
func (h *Certifier) generateCert(block *Block) *QuasarCert {
|
|
h.mu.RLock()
|
|
signer := h.signer
|
|
ctx := h.signerCtx
|
|
validatorCount := len(h.validators)
|
|
demandsTriple := h.demandsTriple()
|
|
h.mu.RUnlock()
|
|
|
|
if signer != nil {
|
|
cert := h.realCert(ctx, signer, block, validatorCount)
|
|
if cert != nil {
|
|
// Under a triple-mode profile we MUST refuse a cert that
|
|
// doesn't carry every layer. The realCert path produces
|
|
// BLS + MLDSAProof but intentionally leaves Corona
|
|
// empty when this engine signs as a single participant
|
|
// (the Round-1 commitment isn't a verifiable sig on its
|
|
// own — the aggregated Corona signature lands here at
|
|
// the higher protocol layer, see epoch.go BundleSigner).
|
|
//
|
|
// Strict-PQ profiles can't emit such a cert. The right
|
|
// architectural answer is to construct this Certifier
|
|
// with a signer that completes all three layers at this
|
|
// point, or to advance the round via the BundleSigner
|
|
// path. Returning nil here surfaces the misconfiguration
|
|
// as "round did not finalise" rather than silently
|
|
// emitting a downgraded cert.
|
|
if demandsTriple {
|
|
if cert.Corona == nil || len(cert.MLDSARollup) == 0 || len(cert.BLS) == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
return cert
|
|
}
|
|
// realCert returned nil (no configured validator with the
|
|
// required keys). Under a triple-mode profile we refuse to
|
|
// fall through to the SHA-256 placeholder — the placeholder
|
|
// is structurally indistinguishable from a real cert on the
|
|
// wire and would silently downgrade.
|
|
if demandsTriple {
|
|
return nil
|
|
}
|
|
// Fall through to SHA-256 placeholder if real signing fails (no
|
|
// configured validator with all three keys), so the engine still
|
|
// makes progress in test scenarios. Production callers should
|
|
// always have a fully-configured signer.
|
|
}
|
|
|
|
// No signer attached: never admissible under a triple-mode profile.
|
|
if demandsTriple {
|
|
return nil
|
|
}
|
|
|
|
// Legacy SHA-256 placeholder (only used when no signer is attached).
|
|
blsData := sha256.Sum256(block.ID[:])
|
|
pqData := sha256.Sum256(append(block.ID[:], block.ChainID[:]...))
|
|
|
|
return &QuasarCert{
|
|
BLS: blsData[:],
|
|
MLDSARollup: pqData[:],
|
|
Epoch: block.Height,
|
|
Finality: time.Now(),
|
|
Validators: validatorCount,
|
|
}
|
|
}
|
|
|
|
// realCert produces a real QuasarCert by driving the signer's
|
|
// TripleSignRound1 path. Returns nil if no validator is configured for all
|
|
// three signing paths (the caller falls back to the legacy placeholder).
|
|
func (h *Certifier) realCert(ctx context.Context, s *signer, block *Block, validatorCount int) *QuasarCert {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Pick any configured validator (the engine itself signs as one
|
|
// participant; full quorum aggregation happens at a higher layer when
|
|
// multiple validators contribute shares).
|
|
var validatorID string
|
|
s.mu.RLock()
|
|
for id := range s.blsSigners {
|
|
validatorID = id
|
|
break
|
|
}
|
|
if validatorID == "" {
|
|
for id := range s.blsKeys {
|
|
validatorID = id
|
|
break
|
|
}
|
|
}
|
|
s.mu.RUnlock()
|
|
if validatorID == "" {
|
|
return nil
|
|
}
|
|
|
|
msg := buildBlockMessage(block)
|
|
prfKey := buildPRFKey(block)
|
|
sessionID := int(block.Height)
|
|
|
|
sig, _, err := s.TripleSignRound1(ctx, validatorID, msg, sessionID, prfKey)
|
|
if err != nil {
|
|
// TripleSignRound1 requires a BLS threshold signer. Fall back to
|
|
// legacy single-key BLS+ML-DSA via SignMessageWithContext.
|
|
legacy, lerr := s.SignMessageWithContext(ctx, validatorID, msg)
|
|
if lerr != nil {
|
|
return nil
|
|
}
|
|
sig = legacy
|
|
}
|
|
|
|
cert := &QuasarCert{
|
|
BLS: append([]byte(nil), sig.BLS...),
|
|
Corona: nil, // Corona Round 1 commitment is not a verifiable
|
|
// signature on its own; aggregation/Round2 is run by the
|
|
// consensus driver. We leave Corona empty here -- it's wired
|
|
// at the higher protocol layer (epoch.go BundleSigner).
|
|
Epoch: block.Height,
|
|
Finality: time.Now(),
|
|
Validators: validatorCount,
|
|
}
|
|
|
|
if len(sig.MLDSA) > 0 {
|
|
cert.MLDSARollup = EncodeMLDSASigs([][]byte{sig.MLDSA})
|
|
}
|
|
|
|
return cert
|
|
}
|
|
|
|
// buildBlockMessage builds the canonical message bytes that the signer
|
|
// commits to for a block. Stable across encode/decode of QuasarCert.
|
|
func buildBlockMessage(block *Block) []byte {
|
|
h := sha256.New()
|
|
h.Write(block.ID[:])
|
|
h.Write(block.ChainID[:])
|
|
var buf [8]byte
|
|
for i := 0; i < 8; i++ {
|
|
buf[i] = byte(block.Height >> (8 * (7 - i)))
|
|
}
|
|
h.Write(buf[:])
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// buildPRFKey derives a per-block 32-byte PRF key for Corona signing.
|
|
func buildPRFKey(block *Block) []byte {
|
|
h := sha256.Sum256(append(block.ID[:], block.ChainID[:]...))
|
|
return h[:]
|
|
}
|
|
|
|
// AddValidator adds a validator to the consensus.
|
|
func (h *Certifier) AddValidator(id string, weight int) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.validators[id] = weight
|
|
}
|
|
|
|
// RemoveValidator removes a validator from the consensus.
|
|
func (h *Certifier) RemoveValidator(id string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
delete(h.validators, id)
|
|
}
|