quasar: rename Corona→Corona, MLDSAProof→MLDSARollup, drop "triple"

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).
This commit is contained in:
2026-05-12 08:40:06 -07:00
parent 675ef93fa8
commit d1c8a2cc28
20 changed files with 205 additions and 137 deletions
+5 -5
View File
@@ -276,7 +276,7 @@ func BenchmarkPQModes_BLSPlusMLDSA(b *testing.B) {
cert := &quasar.QuasarCert{
BLS: blsAgg,
MLDSAProof: mldsaPayload,
MLDSARollup: mldsaPayload,
Epoch: 1,
Finality: time.Now(),
Validators: n,
@@ -472,7 +472,7 @@ func BenchmarkPQModes_BLSPlusCorona(b *testing.B) {
cert := &quasar.QuasarCert{
BLS: res.blsAgg,
Corona: res.rtBytes,
Corona: res.rtBytes,
Epoch: 1,
Finality: time.Now(),
Validators: n,
@@ -514,7 +514,7 @@ func BenchmarkPQModes_BLSPlusGroth16(b *testing.B) {
groth16Placeholder := make([]byte, 192)
cert := &quasar.QuasarCert{
BLS: blsAgg,
MLDSAProof: groth16Placeholder,
MLDSARollup: groth16Placeholder,
Epoch: 1,
Finality: time.Now(),
Validators: n,
@@ -619,8 +619,8 @@ func BenchmarkPQModes_TripleQuantum(b *testing.B) {
cert := &quasar.QuasarCert{
BLS: res.blsAgg,
Corona: res.rtBytes,
MLDSAProof: mldsaPayload,
Corona: res.rtBytes,
MLDSARollup: mldsaPayload,
Epoch: 1,
Finality: time.Now(),
Validators: n,
+1 -1
View File
@@ -43,7 +43,7 @@ Each cryptographic layer is independently toggleable:
- BLS-only: fastest classical consensus (BLS12-381 threshold)
- BLS + ML-DSA: dual with PQ identity proof (FIPS 204)
- BLS + Corona: dual with PQ threshold proof (Ring-LWE)
- BLS + Corona: dual with PQ threshold proof (Ring-LWE)
- BLS + Corona + ML-DSA: full Quasar (all three in parallel)
# Testing
+27 -21
View File
@@ -35,15 +35,19 @@ type ConsensusEngine struct {
func (e *ConsensusEngine) PQMode() config.PQMode { return e.pqMode }
// FinalityEvent represents a finalized block with quantum-resistant proofs.
// Carries all three components for parallel verification: classical BLS,
// lattice Corona, and FIPS 204 ML-DSA. Defense in depth.
// Carries the configured layers for parallel verification: classical
// BLS fast-path, Corona (Ring-LWE) threshold, optional Pulsar (Module-
// LWE) threshold, and the per-validator ML-DSA-65 identity rollup.
// Defence in depth across one classical curve and (one or two) PQ
// lattice families.
type FinalityEvent struct {
Height uint64
BlockID ids.ID
Timestamp time.Time
BLSProof []byte // Classical BLS12-381 aggregate
Corona []byte // Corona lattice threshold sig
MLDSAProof []byte // FIPS 204 ML-DSA proof
Height uint64
BlockID ids.ID
Timestamp time.Time
BLSProof []byte // Classical BLS-12-381 aggregate (optional)
Corona []byte // Corona (Ring-LWE) threshold sig
Pulsar []byte // Pulsar-M (Module-LWE) threshold sig (optional)
MLDSARollup []byte // Per-validator ML-DSA-65 rolled up via STARK/Groth16
}
// memoryStore is a simple in-memory implementation of dag.Store for P-Chain vertices
@@ -201,11 +205,11 @@ func (e *ConsensusEngine) ProcessBlock(ctx context.Context, blockID ids.ID, vote
// Emit finality event
select {
case e.finality <- FinalityEvent{
Height: e.height,
BlockID: blockID,
Timestamp: time.Now(),
MLDSAProof: cert.PQCert,
BLSProof: cert.BLSAgg,
Height: e.height,
BlockID: blockID,
Timestamp: time.Now(),
MLDSARollup: cert.PQCert,
BLSProof: cert.BLSAgg,
}:
case <-ctx.Done():
return ctx.Err()
@@ -238,19 +242,21 @@ func (e *ConsensusEngine) Height() uint64 {
// blockToFinalityEvent converts a quasar.Block to a FinalityEvent
func blockToFinalityEvent(block *quasar.Block) FinalityEvent {
blockID, _ := ids.FromString(block.Hash)
var mldsaProof, corona, blsProof []byte
var mldsaRollup, corona, pulsar, blsProof []byte
if block.Cert != nil {
mldsaProof = block.Cert.MLDSAProof
mldsaRollup = block.Cert.MLDSARollup
corona = block.Cert.Corona
pulsar = block.Cert.Pulsar
blsProof = block.Cert.BLS
}
return FinalityEvent{
Height: block.Height,
BlockID: blockID,
Timestamp: block.Timestamp,
MLDSAProof: mldsaProof,
Corona: corona,
BLSProof: blsProof,
Height: block.Height,
BlockID: blockID,
Timestamp: block.Timestamp,
MLDSARollup: mldsaRollup,
Corona: corona,
Pulsar: pulsar,
BLSProof: blsProof,
}
}
+9 -9
View File
@@ -273,7 +273,7 @@ func TestProcessBlockEmptyCertificates(t *testing.T) {
select {
case event := <-engine.FinalityChannel():
require.Equal(t, blockID, event.BlockID)
require.NotEmpty(t, event.MLDSAProof)
require.NotEmpty(t, event.MLDSARollup)
require.NotEmpty(t, event.BLSProof)
case <-time.After(100 * time.Millisecond):
t.Fatal("Expected finality event")
@@ -285,14 +285,14 @@ func TestFinalityEventFields(t *testing.T) {
Height: 100,
BlockID: ids.GenerateTestID(),
Timestamp: time.Now(),
MLDSAProof: []byte("pq-proof"),
MLDSARollup: []byte("pq-proof"),
BLSProof: []byte("bls-proof"),
}
require.Equal(t, uint64(100), event.Height)
require.NotEmpty(t, event.BlockID)
require.False(t, event.Timestamp.IsZero())
require.Equal(t, []byte("pq-proof"), event.MLDSAProof)
require.Equal(t, []byte("pq-proof"), event.MLDSARollup)
require.Equal(t, []byte("bls-proof"), event.BLSProof)
}
@@ -472,7 +472,7 @@ func TestBlockToFinalityEvent(t *testing.T) {
Timestamp: testTime,
Cert: &quasar.QuasarCert{
BLS: []byte("bls-proof-data"),
MLDSAProof: []byte("pq-proof-data"),
MLDSARollup: []byte("pq-proof-data"),
},
},
wantBLS: []byte("bls-proof-data"),
@@ -497,7 +497,7 @@ func TestBlockToFinalityEvent(t *testing.T) {
Timestamp: testTime,
Cert: &quasar.QuasarCert{
BLS: []byte{},
MLDSAProof: []byte{},
MLDSARollup: []byte{},
},
},
wantBLS: []byte{},
@@ -534,7 +534,7 @@ func TestBlockToFinalityEvent(t *testing.T) {
require.Equal(t, tc.block.Height, event.Height)
require.Equal(t, tc.block.Timestamp, event.Timestamp)
require.Equal(t, tc.wantBLS, event.BLSProof)
require.Equal(t, tc.wantPQ, event.MLDSAProof)
require.Equal(t, tc.wantPQ, event.MLDSARollup)
})
}
}
@@ -613,7 +613,7 @@ func TestSetFinalizedCallbackInvocation(t *testing.T) {
Timestamp: time.Now(),
Cert: &quasar.QuasarCert{
BLS: []byte("test-bls"),
MLDSAProof: []byte("test-pq"),
MLDSARollup: []byte("test-pq"),
},
}
@@ -622,7 +622,7 @@ func TestSetFinalizedCallbackInvocation(t *testing.T) {
require.True(t, callbackCalled)
require.Equal(t, uint64(42), receivedEvent.Height)
require.Equal(t, []byte("test-bls"), receivedEvent.BLSProof)
require.Equal(t, []byte("test-pq"), receivedEvent.MLDSAProof)
require.Equal(t, []byte("test-pq"), receivedEvent.MLDSARollup)
}
// TestSetFinalizedCallbackInvocationNilCert tests callback with nil certificate
@@ -663,5 +663,5 @@ func TestSetFinalizedCallbackInvocationNilCert(t *testing.T) {
require.True(t, callbackCalled)
require.Equal(t, uint64(100), receivedEvent.Height)
require.Nil(t, receivedEvent.BLSProof)
require.Nil(t, receivedEvent.MLDSAProof)
require.Nil(t, receivedEvent.MLDSARollup)
}
+1 -1
View File
@@ -175,7 +175,7 @@ func (pq *PostQuantum) GenerateQuantumProof(ctx context.Context, blockID ids.ID)
BLS: append([]byte(nil), sig.BLS...),
}
if len(sig.MLDSA) > 0 {
cert.MLDSAProof = quasar.EncodeMLDSASigs([][]byte{sig.MLDSA})
cert.MLDSARollup = quasar.EncodeMLDSASigs([][]byte{sig.MLDSA})
}
return cert.MarshalBinary()
}
+1 -1
View File
@@ -122,7 +122,7 @@ func TestFinalityChannel(t *testing.T) {
if event.Height != 1 {
t.Errorf("Wrong height in event: got %d, want 1", event.Height)
}
if len(event.MLDSAProof) == 0 {
if len(event.MLDSARollup) == 0 {
t.Error("Missing PQ proof in event")
}
if len(event.BLSProof) == 0 {
+2 -2
View File
@@ -622,8 +622,8 @@ func (p *QuantumPolicy) MaybeFinalize(ctx context.Context, candidateID Candidate
// embed individual ML-DSA-65 signatures in MLDSAProof.
qc := &quasar.QuasarCert{
BLS: concatSignatures(p.blsVotes[candidateID]),
Corona: concatSignatures(p.pqVotes[candidateID]),
MLDSAProof: nil,
Corona: concatSignatures(p.pqVotes[candidateID]),
MLDSARollup: nil,
Epoch: candidate.Height,
Finality: time.Now(),
Validators: blsCount,
+3 -3
View File
@@ -119,7 +119,7 @@ func TestRealThresholdSig_Accepted(t *testing.T) {
func TestQuasarCert_GarbageBytes_Rejected(t *testing.T) {
cert := &QuasarCert{
BLS: []byte{0x01},
MLDSAProof: []byte{0x01},
MLDSARollup: []byte{0x01},
Validators: 1,
}
@@ -145,7 +145,7 @@ func TestQuasarCert_NilCert_Rejected(t *testing.T) {
func TestQuasarCert_EmptyBLS_Rejected(t *testing.T) {
cert := &QuasarCert{
BLS: []byte{},
MLDSAProof: []byte{0x01, 0x02},
MLDSARollup: []byte{0x01, 0x02},
}
require.False(t, cert.VerifyWithKeys([]byte("key"), []byte("key")),
"QuasarCert with empty BLS must return false")
@@ -155,7 +155,7 @@ func TestQuasarCert_EmptyBLS_Rejected(t *testing.T) {
func TestQuasarCert_EmptyPQ_Rejected(t *testing.T) {
cert := &QuasarCert{
BLS: []byte{0x01, 0x02},
MLDSAProof: []byte{},
MLDSARollup: []byte{},
}
require.False(t, cert.VerifyWithKeys([]byte("key"), []byte("key")),
"QuasarCert with empty PQ must return false")
+3 -3
View File
@@ -54,8 +54,8 @@ func TestQuasarCert_RoundTrip_E2E(t *testing.T) {
// end-to-end byte exercise; full aggregation is a higher-layer concern.
cert := &QuasarCert{
BLS: append([]byte(nil), sig.BLS...),
Corona: []byte{0x01}, // structural-only; cryptographic Corona is via coronaGobEncode of a real Signature
MLDSAProof: EncodeMLDSASigs([][]byte{sig.MLDSA}),
Corona: []byte{0x01}, // structural-only; cryptographic Corona is via coronaGobEncode of a real Signature
MLDSARollup: EncodeMLDSASigs([][]byte{sig.MLDSA}),
Epoch: 1,
Finality: time.Unix(1700000000, 0),
Validators: 3,
@@ -83,7 +83,7 @@ func TestQuasarCert_RoundTrip_E2E(t *testing.T) {
if string(got.Corona) != string(cert.Corona) {
t.Fatalf("Corona round-trip mismatch")
}
if string(got.MLDSAProof) != string(cert.MLDSAProof) {
if string(got.MLDSARollup) != string(cert.MLDSARollup) {
t.Fatalf("MLDSAProof round-trip mismatch")
}
if got.Epoch != cert.Epoch {
+8 -8
View File
@@ -613,7 +613,7 @@ func TestQuasarConsensus_GenerateCert(t *testing.T) {
t.Error("expected non-empty BLS")
}
if len(cert.MLDSAProof) == 0 {
if len(cert.MLDSARollup) == 0 {
t.Error("expected non-empty PQ")
}
@@ -690,7 +690,7 @@ func TestSigner_VerifyQuasarSig_ValidatorNotFound(t *testing.T) {
sig := &QuasarSig{
ValidatorID: "nonexistent",
BLS: []byte{1, 2, 3},
Corona: []byte{4, 5, 6},
Corona: []byte{4, 5, 6},
}
result := h.VerifyQuasarSig([]byte("test"), sig)
@@ -706,7 +706,7 @@ func TestSigner_VerifyQuasarSig_InvalidBLS(t *testing.T) {
sig := &QuasarSig{
ValidatorID: "v1",
BLS: []byte{1, 2, 3}, // Invalid BLS bytes
Corona: []byte{4, 5, 6},
Corona: []byte{4, 5, 6},
}
result := h.VerifyQuasarSig([]byte("test"), sig)
@@ -886,7 +886,7 @@ func TestQuasarCert_Verify(t *testing.T) {
name: "empty BLS",
cert: &QuasarCert{
BLS: nil,
MLDSAProof: []byte{1, 2, 3},
MLDSARollup: []byte{1, 2, 3},
},
validators: []string{"v1"},
want: false,
@@ -895,7 +895,7 @@ func TestQuasarCert_Verify(t *testing.T) {
name: "empty PQ",
cert: &QuasarCert{
BLS: []byte{1, 2, 3},
MLDSAProof: nil,
MLDSARollup: nil,
},
validators: []string{"v1"},
want: false,
@@ -904,7 +904,7 @@ func TestQuasarCert_Verify(t *testing.T) {
name: "non-empty cert without crypto verification returns false",
cert: &QuasarCert{
BLS: []byte{1, 2, 3},
MLDSAProof: []byte{4, 5, 6},
MLDSARollup: []byte{4, 5, 6},
Validators: 0,
},
validators: []string{"v1", "v2"},
@@ -934,7 +934,7 @@ func TestBlock_Fields(t *testing.T) {
Data: []byte("test data"),
Cert: &QuasarCert{
BLS: []byte{7, 8, 9},
MLDSAProof: []byte{10, 11, 12},
MLDSARollup: []byte{10, 11, 12},
Validators: 1,
Epoch: 100,
Finality: now,
@@ -1867,7 +1867,7 @@ func TestSigner_AggregateSignatures_InvalidBLS(t *testing.T) {
sig := &QuasarSig{
ValidatorID: "v1",
BLS: []byte{1, 2, 3}, // Invalid BLS bytes
Corona: []byte{4, 5, 6},
Corona: []byte{4, 5, 6},
}
_, err := h.AggregateSignatures([]byte("test"), []*QuasarSig{sig})
+1 -1
View File
@@ -9,7 +9,7 @@
//
// BLS-only: fastest classical consensus
// BLS + ML-DSA: dual PQ consensus (single-round PQ sigs)
// BLS + Corona: dual PQ consensus (2-round threshold)
// BLS + Corona: dual PQ consensus (2-round threshold)
// BLS + Corona + ML-DSA: Quasar (all three hardness assumptions)
//
// Quasar signing via [signer.TripleSignRound1] runs all three paths in parallel.
+2 -2
View File
@@ -257,9 +257,9 @@ func TestDualSigningFlow(t *testing.T) {
// Verify Corona
rtValid := coronaThreshold.Verify(rtGroupKey, messageStr, rtSig)
require.True(t, rtValid, "Corona signature verification failed")
t.Log(" Corona: ✓ verified")
t.Log(" Corona: ✓ verified")
t.Log("✓ Dual BLS + Corona signing flow complete")
t.Log(" - BLS: 1 round, aggregated, verified")
t.Log(" - Corona: 2 rounds, aggregated, verified (post-quantum)")
t.Log(" - Corona: 2 rounds, aggregated, verified (post-quantum)")
}
+8 -8
View File
@@ -343,7 +343,7 @@ func (h *Certifier) generateCert(block *Block) *QuasarCert {
// as "round did not finalise" rather than silently
// emitting a downgraded cert.
if demandsTriple {
if cert.Corona == nil || len(cert.MLDSAProof) == 0 || len(cert.BLS) == 0 {
if cert.Corona == nil || len(cert.MLDSARollup) == 0 || len(cert.BLS) == 0 {
return nil
}
}
@@ -373,11 +373,11 @@ func (h *Certifier) generateCert(block *Block) *QuasarCert {
pqData := sha256.Sum256(append(block.ID[:], block.ChainID[:]...))
return &QuasarCert{
BLS: blsData[:],
MLDSAProof: pqData[:],
Epoch: block.Height,
Finality: time.Now(),
Validators: validatorCount,
BLS: blsData[:],
MLDSARollup: pqData[:],
Epoch: block.Height,
Finality: time.Now(),
Validators: validatorCount,
}
}
@@ -425,7 +425,7 @@ func (h *Certifier) realCert(ctx context.Context, s *signer, block *Block, valid
}
cert := &QuasarCert{
BLS: append([]byte(nil), sig.BLS...),
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
@@ -436,7 +436,7 @@ func (h *Certifier) realCert(ctx context.Context, s *signer, block *Block, valid
}
if len(sig.MLDSA) > 0 {
cert.MLDSAProof = EncodeMLDSASigs([][]byte{sig.MLDSA})
cert.MLDSARollup = EncodeMLDSASigs([][]byte{sig.MLDSA})
}
return cert
+2 -2
View File
@@ -246,7 +246,7 @@ func TestBlock(t *testing.T) {
Timestamp: time.Now(),
Cert: &QuasarCert{
BLS: []byte("bls"),
MLDSAProof: []byte("pq"),
MLDSARollup: []byte("pq"),
},
}
@@ -328,7 +328,7 @@ func TestQuantumFinality_Integration(t *testing.T) {
Timestamp: time.Now(),
Cert: &QuasarCert{
BLS: cert.BLSAgg,
MLDSAProof: cert.PQCert,
MLDSARollup: cert.PQCert,
},
}
+3 -3
View File
@@ -84,7 +84,7 @@ func BenchmarkAggregateSignatures(b *testing.B) {
// Copy to avoid pool reuse during benchmark
sigs[i] = &QuasarSig{
BLS: append([]byte(nil), sig.BLS...),
Corona: append([]byte(nil), sig.Corona...),
Corona: append([]byte(nil), sig.Corona...),
ValidatorID: sig.ValidatorID,
}
ReleaseQuasarSig(sig)
@@ -126,7 +126,7 @@ func BenchmarkVerifyAggregatedSignature(b *testing.B) {
}
sigs[i] = &QuasarSig{
BLS: append([]byte(nil), sig.BLS...),
Corona: append([]byte(nil), sig.Corona...),
Corona: append([]byte(nil), sig.Corona...),
ValidatorID: sig.ValidatorID,
}
ReleaseQuasarSig(sig)
@@ -173,7 +173,7 @@ func BenchmarkVerifyAggregatedSignatureParallel(b *testing.B) {
}
sigs[i] = &QuasarSig{
BLS: append([]byte(nil), sig.BLS...),
Corona: append([]byte(nil), sig.Corona...),
Corona: append([]byte(nil), sig.Corona...),
ValidatorID: sig.ValidatorID,
}
ReleaseQuasarSig(sig)
+8 -7
View File
@@ -30,7 +30,7 @@ var (
New: func() any {
return &QuasarSig{
BLS: make([]byte, 0, 96),
Corona: make([]byte, 0, 4096),
Corona: make([]byte, 0, 4096),
MLDSA: make([]byte, 0, 3309), // ML-DSA-65 sig size
}
},
@@ -55,7 +55,7 @@ var (
//
// Three independent signing paths:
// - BLS12-381: classical threshold signatures (ECDL hardness)
// - Corona: Ring-LWE 2-round threshold signatures (Module-LWE hardness)
// - Corona: Ring-LWE 2-round threshold signatures (Module-LWE hardness)
// - ML-DSA-65: FIPS 204 post-quantum identity signatures (Module-LWE + Module-SIS)
//
// All three run in parallel via TripleSign. Each can be enabled independently.
@@ -315,7 +315,7 @@ func (s *signer) VerifyCoronaSignature(message string, sig *coronaThreshold.Sign
// DualSignRound1 performs Round 1 of both BLS and Corona in parallel.
// BLS: Computes signature share (single round)
// Corona: Computes D matrix + MACs (Round 1 of 2)
// Corona: Computes D matrix + MACs (Round 1 of 2)
func (s *signer) DualSignRound1(ctx context.Context, validatorID string, message []byte, sessionID int, prfKey []byte) (*QuasarSig, *coronaThreshold.Round1Data, error) {
s.mu.RLock()
blsSigner, hasBLS := s.blsSigners[validatorID]
@@ -381,7 +381,7 @@ func (s *signer) DualSignRound2(validatorID string, sessionID int, message strin
// TripleSignRound1 performs Round 1 of all three signing paths in parallel:
// - BLS: threshold share (single round, complete)
// - Corona: Round 1 of 2-round protocol (D matrix + MACs)
// - Corona: Round 1 of 2-round protocol (D matrix + MACs)
// - ML-DSA: full signature (single round, complete)
//
// Returns the QuasarSig with BLS + MLDSA filled, plus Corona Round1Data
@@ -775,9 +775,10 @@ func (s *signer) VerifyAggregatedSignatureWithContext(ctx context.Context, messa
// QuasarSig contains BLS and optionally Corona signature data.
type QuasarSig struct {
BLS []byte // BLS12-381 signature (classical)
Corona []byte // Corona Ring-LWE signature (PQ threshold)
MLDSA []byte // ML-DSA-65 signature (PQ identity, FIPS 204)
BLS []byte // BLS-12-381 aggregate (classical fast-path; empty in pure-PQ)
Corona []byte // Corona (Ring-LWE) threshold signature
Pulsar []byte // Pulsar-M (Module-LWE) threshold signature
MLDSA []byte // Per-validator ML-DSA-65 (FIPS 204 identity attestation)
ValidatorID string
IsThreshold bool
SignerIndex int
+4 -4
View File
@@ -80,7 +80,7 @@ func TestCRITICAL1_ThresholdSigWithoutVerifier(t *testing.T) {
func TestCRITICAL2_QuasarCertVerifyRejectGarbage(t *testing.T) {
cert := &QuasarCert{
BLS: []byte("garbage-bls-not-a-real-signature"),
MLDSAProof: []byte("garbage-pq-not-a-real-certificate"),
MLDSARollup: []byte("garbage-pq-not-a-real-certificate"),
Validators: 0,
}
@@ -99,15 +99,15 @@ func TestCRITICAL2_QuasarCertVerifyWithKeysNilCert(t *testing.T) {
// BLS or PQ fields are rejected.
func TestCRITICAL2_QuasarCertVerifyWithKeysEmptyFields(t *testing.T) {
// Empty BLS
cert := &QuasarCert{BLS: nil, MLDSAProof: []byte("pq")}
cert := &QuasarCert{BLS: nil, MLDSARollup: []byte("pq")}
require.False(t, cert.VerifyWithKeys([]byte("key"), []byte("pq")))
// Empty PQ
cert = &QuasarCert{BLS: []byte("bls"), MLDSAProof: nil}
cert = &QuasarCert{BLS: []byte("bls"), MLDSARollup: nil}
require.False(t, cert.VerifyWithKeys([]byte("key"), []byte("pq")))
// Empty group key
cert = &QuasarCert{BLS: []byte("bls"), MLDSAProof: []byte("pq")}
cert = &QuasarCert{BLS: []byte("bls"), MLDSARollup: []byte("pq")}
require.False(t, cert.VerifyWithKeys(nil, []byte("pq")))
}
+1 -1
View File
@@ -665,7 +665,7 @@ func TestQuasarConsensusBasic(t *testing.T) {
cert := hybrid.generateCert(block)
require.NotNil(cert)
require.NotEmpty(cert.BLS)
require.NotEmpty(cert.MLDSAProof)
require.NotEmpty(cert.MLDSARollup)
require.Equal(uint64(1), cert.Epoch)
// Verify certificate: Verify() now always returns false (requires VerifyWithKeys)
+7 -7
View File
@@ -74,7 +74,7 @@ func TestGenerateCert_RefusesPartialTriple_WithSigner_NoCorona(t *testing.T) {
cert := c.generateCert(block)
if cert != nil {
t.Fatalf("strict-PQ profile + basic signer: expected nil cert, got Corona=%v MLDSAProof_len=%d",
cert.Corona, len(cert.MLDSAProof))
cert.Corona, len(cert.MLDSARollup))
}
}
@@ -134,8 +134,8 @@ func TestGenerateCert_NilProfile_AcceptsPlaceholder(t *testing.T) {
func TestQuasarCert_Verify_RejectsMissingCorona(t *testing.T) {
cert := &QuasarCert{
BLS: []byte("bls"),
Corona: nil, // missing
MLDSAProof: []byte("mldsa"),
Corona: nil, // missing
MLDSARollup: []byte("mldsa"),
Epoch: 1,
Finality: time.Now(),
Validators: 1,
@@ -150,8 +150,8 @@ func TestQuasarCert_Verify_RejectsMissingCorona(t *testing.T) {
func TestQuasarCert_Verify_RejectsMissingMLDSA(t *testing.T) {
cert := &QuasarCert{
BLS: []byte("bls"),
Corona: []byte("rt"),
MLDSAProof: nil, // missing
Corona: []byte("rt"),
MLDSARollup: nil, // missing
Epoch: 1,
Finality: time.Now(),
Validators: 1,
@@ -165,8 +165,8 @@ func TestQuasarCert_Verify_RejectsMissingMLDSA(t *testing.T) {
func TestQuasarCert_Verify_RejectsMissingBLS(t *testing.T) {
cert := &QuasarCert{
BLS: nil, // missing
Corona: []byte("rt"),
MLDSAProof: []byte("mldsa"),
Corona: []byte("rt"),
MLDSARollup: []byte("mldsa"),
Epoch: 1,
Finality: time.Now(),
Validators: 1,
+109 -48
View File
@@ -29,37 +29,93 @@ type Block struct {
// QuasarCert is the finality certificate for Quasar consensus.
//
// Three signature layers, each already compact on its own except ML-DSA:
// Architectural shape — read the cardinality, not a counting word:
//
// BLS12-381 — classical aggregate (48 bytes, O(1) via native BLS aggregation)
// Corona — lattice threshold signature (O(1) after DKG threshold signing)
// ML-DSA-65 — per-validator PQ identity (3309 bytes × N validators, O(N))
// BLS-12-381 — classical fast-path aggregate (48 B; pairing-based)
// Corona — Ring-LWE threshold ML-DSA (O(1) after DKG; FIPS 204 Class N1)
// Pulsar — Module-LWE threshold ML-DSA (O(1) after DKG; FIPS 204 Class N1)
// MLDSARollup — per-validator ML-DSA-65 rolled into one STARK / Groth16 proof
//
// Z-Chain's sole job is to roll up ML-DSA × N into a single Groth16 SNARK:
// Naming: the PQ-side is at most **Double Lattice** — Ring-LWE plus
// Module-LWE. Calling the BLS+Corona+MLDSARollup composition "Triple
// Quantum" is misleading because BLS is classical, not quantum. The
// honest mode descriptor is "Classical + Double Lattice [+ Identity
// Rollup]".
//
// Statement: ∀ i ∈ [N]: ML-DSA.Verify(mldsa_pk_i, msg, mldsa_σ_i) = 1
// Output: 192-byte Groth16 proof (MLDSAProof)
// Pure-PQ profile (no BLS, no classical curve in the validator path):
// Corona + Pulsar (Double Lattice). Each layer is independently
// FIPS 204 Class N1 (output verifies under the unmodified single-
// party ML-DSA verifier). MLDSARollup is the identity attestation
// layer, succinct via STARK (Z-Chain → P3Q).
//
// BLS and Corona don't need Z-Chain — they're already O(1).
// Corona (academic R-LWE with trusted-dealer DKG) is NOT a
// production option for an open public chain. Corona is the
// production Ring-LWE library (Pedersen DKG over R_q + proactive
// resharing; `luxfi/corona`). The historic field name "Corona"
// in this struct refers to "the Ring-LWE threshold slot"; that
// slot now carries Corona bytes. The field is renamed accordingly.
//
// Total cert size: 48 (BLS) + |Corona| + 192 (MLDSAProof), constant in N.
// MLDSARollup is succinct: a single STARK/Groth16 proof attests
// "∀ i ∈ [N]: ML-DSA.Verify(mldsa_pk_i, msg, mldsa_σ_i) = 1". Output
// is one constant-size proof (192 B for Groth16, ~30 KB for a STARK
// like P3Q), constant in validator count.
//
// Total cert size: 48 (BLS, optional) + |Corona| + |Pulsar| + |MLDSARollup|.
type QuasarCert struct {
BLS []byte // BLS12-381 aggregate (48 bytes, classical)
Corona []byte // Corona lattice threshold sig (O(1) after DKG)
MLDSAProof []byte // Z-Chain Groth16 rolling up N × ML-DSA identity sigs (192 bytes)
Epoch uint64 // Epoch number
Finality time.Time // Time of finality
Validators int `json:"validators,omitempty"` // Count of signing validators
BLS []byte // BLS-12-381 aggregate (48 bytes, classical fast-path; empty in pure-PQ)
Corona []byte // Ring-LWE threshold sig (Corona; O(1) after DKG)
Pulsar []byte // Module-LWE threshold sig (Pulsar-M; O(1) after DKG; optional)
MLDSARollup []byte // Succinct rollup of per-validator ML-DSA-65 (STARK or Groth16)
Epoch uint64 // Epoch number
Finality time.Time // Time of finality
Validators int `json:"validators,omitempty"` // Count of signing validators
}
// Verify checks structural presence of BLS and PQ certificates.
// Use VerifyWithKeys for cryptographic verification. Returns true only if
// all three signature fields are non-empty -- this is a fast structural gate.
// IsDoubleLattice reports whether the cert carries both Ring-LWE
// (Corona) and Module-LWE (Pulsar) threshold legs. This is the honest
// post-quantum cardinality: two independent algebraic-lattice families,
// each independently a FIPS 204 Class N1 threshold producer.
//
// BLS is irrelevant to this predicate by design — a pure-PQ cert has
// no BLS bytes; a hybrid cert layers BLS over a Double-Lattice PQ
// surface. Either way, the PQ-side cardinality is what IsDoubleLattice
// names.
func (c *QuasarCert) IsDoubleLattice() bool {
if c == nil {
return false
}
return len(c.Corona) > 0 && len(c.Pulsar) > 0
}
// HasClassicalFastPath reports whether the cert carries BLS bytes — the
// optional classical aggregate that rides alongside the PQ surface on
// chains that opt into the hybrid posture. Empty in pure-PQ mode.
func (c *QuasarCert) HasClassicalFastPath() bool {
if c == nil {
return false
}
return len(c.BLS) > 0
}
// HasIdentityRollup reports whether the cert carries the
// per-validator ML-DSA-65 identity attestation rolled up into a
// succinct STARK/Groth16 proof.
func (c *QuasarCert) HasIdentityRollup() bool {
if c == nil {
return false
}
return len(c.MLDSARollup) > 0
}
// Verify checks structural presence of the Quasar layers: BLS fast-path
// + Corona (Ring-LWE) + MLDSARollup. Use VerifyWithRealKeys for
// cryptographic verification. Returns false if any required slot is
// empty — fast structural gate.
func (c *QuasarCert) Verify(validators []string) bool {
if c == nil {
return false
}
if len(c.BLS) == 0 || len(c.Corona) == 0 || len(c.MLDSAProof) == 0 {
if len(c.BLS) == 0 || len(c.Corona) == 0 || len(c.MLDSARollup) == 0 {
return false
}
if c.Validators > 0 && len(validators) > 0 && len(validators) < c.Validators {
@@ -68,15 +124,15 @@ func (c *QuasarCert) Verify(validators []string) bool {
return true
}
// VerifyWithKeys is a back-compat structural check that mirrors the previous
// API: returns false unless all three signature fields are present and the
// provided opaque key material is non-empty. Real cryptographic verification
// is in VerifyWithRealKeys.
// VerifyWithKeys is a back-compat structural check that mirrors the
// previous API: returns false unless BLS + Corona + MLDSARollup are
// present and the provided opaque key material is non-empty. Real
// cryptographic verification is in VerifyWithRealKeys.
func (c *QuasarCert) VerifyWithKeys(groupKey []byte, pqKey []byte) bool {
if c == nil || len(groupKey) == 0 {
return false
}
if len(c.BLS) == 0 || len(c.Corona) == 0 || len(c.MLDSAProof) == 0 {
if len(c.BLS) == 0 || len(c.Corona) == 0 || len(c.MLDSARollup) == 0 {
return false
}
// The structural check passed; cryptographic verification requires
@@ -84,18 +140,22 @@ func (c *QuasarCert) VerifyWithKeys(groupKey []byte, pqKey []byte) bool {
return false
}
// VerifyWithRealKeys performs cryptographic verification of the certificate.
// A Quasar cert is finalized iff all three components verify in parallel:
// - BLS12-381 aggregate against blsAggPubKey (classical, fast path)
// - Corona threshold sig against rtGroupKey (lattice PQ)
// VerifyWithRealKeys performs cryptographic verification of the
// certificate. A Quasar cert is finalised iff the configured layers
// verify in parallel:
//
// - BLS-12-381 aggregate against blsAggPubKey (classical fast path)
// - Corona (Ring-LWE threshold ML-DSA) against rtGroupKey (PQ)
// - Per-validator ML-DSA-65 sigs against mldsaPubKeys (FIPS 204 PQ)
//
// Defense in depth: classical AND two independent PQ schemes. Any single
// scheme broken does not break finality.
// Defence-in-depth posture is classical + (one or two PQ lattice
// schemes) + identity rollup. Any single scheme broken does not break
// finality.
//
// message is the digest the signers committed to. mldsaPubKeys may be nil
// if MLDSAProof is empty (PQ rollup not yet wired). nil rtGroupKey skips
// the Corona check (used when running in BLS-only mode).
// message is the digest the signers committed to. mldsaPubKeys may be
// nil if MLDSARollup is empty (rollup not yet wired). nil rtGroupKey
// skips the Corona check (used when running in BLS-only mode or in a
// Pulsar-only pure-PQ posture).
func (c *QuasarCert) VerifyWithRealKeys(message []byte, blsAggPubKey *bls.PublicKey, rtGroupKey *coronaThreshold.GroupKey, mldsaPubKeys []*mldsa.PublicKey) bool {
if c == nil || len(message) == 0 {
return false
@@ -116,8 +176,8 @@ func (c *QuasarCert) VerifyWithRealKeys(message []byte, blsAggPubKey *bls.Public
return false
}
// 2. Corona threshold verify (PQ lattice). Optional: skipped when
// rtGroupKey is nil (BLS-only mode).
// 2. Corona threshold verify (Ring-LWE PQ). Optional: skipped when
// rtGroupKey is nil (BLS-only mode or Pulsar-only pure-PQ).
if rtGroupKey != nil {
if len(c.Corona) == 0 {
return false
@@ -131,20 +191,21 @@ func (c *QuasarCert) VerifyWithRealKeys(message []byte, blsAggPubKey *bls.Public
}
}
// 3. ML-DSA-65 verify (PQ identity, FIPS 204). The MLDSAProof field
// holds either a single Groth16 rollup (Z-Chain) or a concatenation
// of per-validator ML-DSA-65 sigs. We support the per-validator path
// here by serializing each sig with a 4-byte length prefix.
if len(c.MLDSAProof) > 0 {
// 3. ML-DSA-65 verify (PQ identity, FIPS 204). The MLDSARollup field
// holds either a single STARK/Groth16 rollup (Z-Chain → P3Q) or a
// concatenation of per-validator ML-DSA-65 sigs. We support the
// per-validator path here by serialising each sig with a 4-byte
// length prefix.
if len(c.MLDSARollup) > 0 {
if len(mldsaPubKeys) == 0 {
return false
}
sigs, err := decodeMLDSASigs(c.MLDSAProof)
sigs, err := decodeMLDSASigs(c.MLDSARollup)
if err != nil {
return false
}
// Need at least Validators good sigs. If MLDSAProof was a single
// Groth16 rollup, we'd verify it once; for now we require N sigs
// Need at least Validators good sigs. If MLDSARollup was a single
// succinct rollup, we'd verify it once; for now we require N sigs
// matching the public keys provided.
if len(sigs) > len(mldsaPubKeys) {
return false
@@ -186,7 +247,7 @@ func (c *QuasarCert) MarshalBinary() ([]byte, error) {
return nil, errors.New("quasar: signature too large")
}
out := make([]byte, 0, 1+2+len(c.BLS)+2+len(c.Corona)+4+len(c.MLDSAProof)+8+8+2)
out := make([]byte, 0, 1+2+len(c.BLS)+2+len(c.Corona)+4+len(c.MLDSARollup)+8+8+2)
out = append(out, CertSchemeQuasar)
var u16 [2]byte
@@ -201,9 +262,9 @@ func (c *QuasarCert) MarshalBinary() ([]byte, error) {
out = append(out, u16[:]...)
out = append(out, c.Corona...)
binary.BigEndian.PutUint32(u32[:], uint32(len(c.MLDSAProof)))
binary.BigEndian.PutUint32(u32[:], uint32(len(c.MLDSARollup)))
out = append(out, u32[:]...)
out = append(out, c.MLDSAProof...)
out = append(out, c.MLDSARollup...)
binary.BigEndian.PutUint64(u64[:], c.Epoch)
out = append(out, u64[:]...)
@@ -257,7 +318,7 @@ func (c *QuasarCert) UnmarshalBinary(data []byte) error {
if off+mldsaLen > len(data) {
return ErrCertCorrupt
}
c.MLDSAProof = append(c.MLDSAProof[:0], data[off:off+mldsaLen]...)
c.MLDSARollup = append(c.MLDSARollup[:0], data[off:off+mldsaLen]...)
off += mldsaLen
if off+8+8+2 > len(data) {
@@ -377,7 +438,7 @@ type CoronaSignature struct {
// Per-validator (collected during consensus, NOT stored in block):
//
// BLS: sign with BLS key → aggregate into 48 bytes (ECDL)
// Corona: sign with ring-LWE key → PQ threshold proof (Module-LWE)
// Corona: sign with ring-LWE key → PQ threshold proof (Module-LWE)
// ML-DSA: sign with ML-DSA-65 key → PQ identity proof (Module-LWE + Module-SIS)
//
// In QuasarCert (stored in block header):