mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
227 lines
6.5 KiB
Go
227 lines
6.5 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package quasar
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/config"
|
|
)
|
|
|
|
// TestGenerateCert_RefusesPartialTriple is the CR-10 regression: under a
|
|
// strict-PQ profile the certifier MUST NOT emit a single-layer cert.
|
|
// The legacy SHA-256 placeholder path silently downgraded; the realCert
|
|
// path with no Corona aggregate also downgraded. Both close here.
|
|
func TestGenerateCert_RefusesPartialTriple(t *testing.T) {
|
|
c, err := newCertifier(1)
|
|
if err != nil {
|
|
t.Fatalf("newCertifier: %v", err)
|
|
}
|
|
c.SetProfile(config.StrictPQ())
|
|
c.AddValidator("v1", 1)
|
|
|
|
block := &Block{
|
|
ID: [32]byte{1, 2, 3},
|
|
ChainID: [32]byte{4, 5, 6},
|
|
ChainName: "test",
|
|
Height: 100,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// No signer attached → no path to a real cert. Under strict-PQ we
|
|
// MUST refuse rather than fall through to the SHA-256 placeholder.
|
|
cert := c.generateCert(block)
|
|
if cert != nil {
|
|
t.Fatalf("strict-PQ profile + no signer: expected nil cert, got %+v", cert)
|
|
}
|
|
}
|
|
|
|
// TestGenerateCert_RefusesPartialTriple_WithSigner_NoCorona proves
|
|
// the realCert fallback path is also closed: a signer can produce a
|
|
// BLS+MLDSA cert (Corona empty because aggregation is at the
|
|
// BundleSigner layer), and under a triple-mode profile we MUST refuse
|
|
// that single-layer artefact.
|
|
func TestGenerateCert_RefusesPartialTriple_WithSigner_NoCorona(t *testing.T) {
|
|
c, err := newCertifier(1)
|
|
if err != nil {
|
|
t.Fatalf("newCertifier: %v", err)
|
|
}
|
|
c.SetProfile(config.StrictPQ())
|
|
c.AddValidator("v1", 1)
|
|
|
|
// Basic signer: AddValidator creates BLS + MLDSA keys but no
|
|
// Corona share. realCert will produce a cert with Corona=nil;
|
|
// the triple-mode gate MUST refuse it.
|
|
s, err := NewSigner(1)
|
|
if err != nil {
|
|
t.Fatalf("NewSigner: %v", err)
|
|
}
|
|
if err := s.AddValidator("v1", 1); err != nil {
|
|
t.Fatalf("AddValidator: %v", err)
|
|
}
|
|
c.AttachSigner(nil, s)
|
|
|
|
block := &Block{
|
|
ID: [32]byte{7, 8, 9},
|
|
ChainID: [32]byte{1, 2, 3},
|
|
ChainName: "test",
|
|
Height: 101,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
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.MLDSARollup))
|
|
}
|
|
}
|
|
|
|
// TestGenerateCert_PermissiveProfile_AcceptsPlaceholder — non-strict
|
|
// profiles preserve the legacy SHA-256 placeholder path. The CR-10
|
|
// gate is profile-driven, not heuristic.
|
|
func TestGenerateCert_PermissiveProfile_AcceptsPlaceholder(t *testing.T) {
|
|
c, err := newCertifier(1)
|
|
if err != nil {
|
|
t.Fatalf("newCertifier: %v", err)
|
|
}
|
|
c.SetProfile(config.Permissive())
|
|
c.AddValidator("v1", 1)
|
|
|
|
block := &Block{
|
|
ID: [32]byte{1, 2, 3},
|
|
ChainID: [32]byte{4, 5, 6},
|
|
ChainName: "test",
|
|
Height: 102,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
cert := c.generateCert(block)
|
|
if cert == nil {
|
|
t.Fatal("permissive profile + no signer: expected placeholder cert, got nil")
|
|
}
|
|
}
|
|
|
|
// TestGenerateCert_NilProfile_AcceptsPlaceholder — pre-locked-profile
|
|
// callers (legacy unit tests) keep working with nil profile.
|
|
func TestGenerateCert_NilProfile_AcceptsPlaceholder(t *testing.T) {
|
|
c, err := newCertifier(1)
|
|
if err != nil {
|
|
t.Fatalf("newCertifier: %v", err)
|
|
}
|
|
// No SetProfile → nil profile.
|
|
c.AddValidator("v1", 1)
|
|
|
|
block := &Block{
|
|
ID: [32]byte{1, 2, 3},
|
|
ChainID: [32]byte{4, 5, 6},
|
|
ChainName: "test",
|
|
Height: 103,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
cert := c.generateCert(block)
|
|
if cert == nil {
|
|
t.Fatal("nil profile + no signer: expected placeholder cert, got nil")
|
|
}
|
|
}
|
|
|
|
// TestQuasarCert_Verify_RejectsMissingCorona proves the structural
|
|
// gate at the vote-acceptance layer: cert.Verify() refuses a cert that
|
|
// doesn't carry every layer, so a single-layer cert is rejected even
|
|
// if generateCert somehow emits it.
|
|
func TestQuasarCert_Verify_RejectsMissingCorona(t *testing.T) {
|
|
cert := &QuasarCert{
|
|
BLS: []byte("bls"),
|
|
Corona: nil, // missing
|
|
MLDSARollup: []byte("mldsa"),
|
|
Epoch: 1,
|
|
Finality: time.Now(),
|
|
Validators: 1,
|
|
}
|
|
if cert.Verify(nil) {
|
|
t.Fatal("QuasarCert.Verify accepted cert with nil Corona")
|
|
}
|
|
}
|
|
|
|
// TestQuasarCert_Verify_RejectsMissingMLDSA — same shape for the MLDSA
|
|
// layer. Tests for symmetry with the Corona case.
|
|
func TestQuasarCert_Verify_RejectsMissingMLDSA(t *testing.T) {
|
|
cert := &QuasarCert{
|
|
BLS: []byte("bls"),
|
|
Corona: []byte("rt"),
|
|
MLDSARollup: nil, // missing
|
|
Epoch: 1,
|
|
Finality: time.Now(),
|
|
Validators: 1,
|
|
}
|
|
if cert.Verify(nil) {
|
|
t.Fatal("QuasarCert.Verify accepted cert with nil MLDSAProof")
|
|
}
|
|
}
|
|
|
|
// TestQuasarCert_Verify_RejectsMissingBLS — same shape for BLS.
|
|
func TestQuasarCert_Verify_RejectsMissingBLS(t *testing.T) {
|
|
cert := &QuasarCert{
|
|
BLS: nil, // missing
|
|
Corona: []byte("rt"),
|
|
MLDSARollup: []byte("mldsa"),
|
|
Epoch: 1,
|
|
Finality: time.Now(),
|
|
Validators: 1,
|
|
}
|
|
if cert.Verify(nil) {
|
|
t.Fatal("QuasarCert.Verify accepted cert with nil BLS")
|
|
}
|
|
}
|
|
|
|
// TestAddVoteLocked_StrictProfile_RefusesSingleLayerVote is the
|
|
// vote-accept-time gate (CR-10). When the profile is strict-PQ and the
|
|
// signer can produce triple-mode sigs, addVoteLocked MUST refuse any
|
|
// per-validator sig that doesn't carry all required layers.
|
|
func TestAddVoteLocked_StrictProfile_RefusesSingleLayerVote(t *testing.T) {
|
|
q, err := NewTestQuasar(1)
|
|
if err != nil {
|
|
t.Fatalf("NewTestQuasar: %v", err)
|
|
}
|
|
q.SetProfile(config.StrictPQ())
|
|
|
|
// Seed a pending block so addVoteLocked has something to act on.
|
|
q.mu.Lock()
|
|
qBlock := &QuantumBlock{
|
|
Height: 1,
|
|
QuantumHash: "h",
|
|
ValidatorSigs: make(map[string]*QuasarSig),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
q.pendingBlocks["h"] = qBlock
|
|
|
|
// Single-layer sig: only BLS, no MLDSA. Under strict profile this
|
|
// MUST be rejected without running the (real-crypto) verify path.
|
|
bad := &QuasarSig{
|
|
BLS: []byte("just-bls"),
|
|
MLDSA: nil,
|
|
ValidatorID: "v0",
|
|
}
|
|
accepted := q.addVoteLocked("h", "v0", bad)
|
|
q.mu.Unlock()
|
|
if accepted {
|
|
t.Fatal("strict-PQ profile: addVoteLocked accepted single-layer sig")
|
|
}
|
|
}
|
|
|
|
// TestAddVoteLocked_NilProfile_AcceptsValidVotes — legacy callers
|
|
// (no profile) keep accepting the BLS+MLDSA-only votes that the test
|
|
// quasar uses for its self-vote during processBlock.
|
|
func TestAddVoteLocked_NilProfile_PreservesLegacyAcceptance(t *testing.T) {
|
|
q, err := NewTestQuasar(1)
|
|
if err != nil {
|
|
t.Fatalf("NewTestQuasar: %v", err)
|
|
}
|
|
// Profile stays nil — legacy path.
|
|
if q.demandsTripleLocked() {
|
|
t.Fatal("nil profile: demandsTripleLocked = true, want false")
|
|
}
|
|
}
|